Статьи

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

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

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

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

Прежде чем мы начнем писать код, необходимо взглянуть на основы UIKit Dynamics. UIKit Dynamics является частью инфраструктуры UIKit, что означает, что вам не нужно добавлять какие-либо дополнительные платформы для ваших проектов, чтобы использовать ее.

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

Основным компонентом интерфейса UIKit Dynamics является класс UIDynamicAnimator , который также известен как динамический аниматор . Этот класс отвечает за выполнение анимации с использованием физического движка под капотом. Хотя динамический аниматор является сердцем интерфейса UIKit Dynamics, он не может использоваться сам по себе.

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

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

  • UIGravityBehavior : этот подкласс UIDynamicBehavior добавляет гравитации к элементу. В результате предмет перемещается в определенном направлении, определяемом гравитационным поведением.
  • UICollisionBehavior : этот класс определяет, как два элемента сталкиваются друг с другом или как элемент сталкивается с предопределенной границей, видимой или невидимой.
  • UIPushBehavior : как указывает его имя, это поведение дает элементу толчок, он ускоряет элемент. Толчок может быть непрерывным или мгновенным . Поведение с непрерывным толчком постепенно применяет силу толчка, в то время как поведение мгновенного толчка применяет силу в момент добавления поведения.
  • UISnapBehavior : поведение привязки определяет, как элемент привязывается к другому элементу или точке в пространстве. Поведение привязки можно настроить несколькими способами. Например, предмет может быть привязан к точке без какого-либо бодрости или покачиваться на несколько секунд, прежде чем остановиться.
  • UIAttachmentBehavior : поведение вложения определяет, как два динамических элемента связаны друг с другом или как динамический элемент связан с точкой привязки.

Это наиболее важные динамические поведения, предоставляемые в настоящее время интерфейсом UIKit Dynamics. Чтобы эти поведения выполняли свою работу, они должны быть инициализированы, настроены и добавлены к объекту динамического аниматора , который мы рассмотрели ранее.

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

Поведение всегда применяется к динамическим элементам. Динамический элемент — это любой объект, который соответствует протоколу UIDynamicItem . Самое замечательное, что UIView и UICollectionViewLayoutAttributes уже соответствуют этому протоколу. Это означает, что каждое представление в вашем приложении может использовать UIKit Dynamics.

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

  1. elasticity : это свойство определяет эластичность столкновения между динамическими элементами или динамическим элементом и границей. Значение варьируется от 0.0 до 1.0 причем последнее является очень упругим столкновением.
  2. density : это свойство определяет массу динамического элемента с высоким значением, приводящим к тяжелому объекту. Это может быть полезно, если вы не хотите, чтобы динамический элемент перемещался, когда другой элемент сталкивался с ним.
  3. resistance : имя собственности говорит само за себя. Свойство определяет демпфирование скорости динамического элемента.
  4. angularResistance : это похоже на свойство resistance , но свойство angularResistance определяет демпфирование угловой скорости.
  5. friction : это свойство определяет трение или сопротивление двух динамических элементов, которые скользят друг против друга.
  6. allowsRotation : этот параметр просто указывает, может ли динамический элемент вращаться или нет.

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

Я призываю вас посетить официальную документацию Apple и прочитать больше о ключевых классах UIKit Dynamics. Кроме того, после завершения этого урока полезно поэкспериментировать с результатом, чтобы лучше понять концепции и различные способы поведения, определенные UIKit Dynamics.

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

Этот компонент является объектом UIView который скользит и выходит из экрана. Чтобы меню появилось и исчезло, используется простой жест смахивания. Элементы меню перечислены в виде простой таблицы. Анимация, которую вы видите на скриншоте выше, достигается сочетанием ряда динамических поведений.

Начните с запуска Xcode и создайте новый проект. Выберите шаблон приложения Single View в категории « Приложения» раздела iOS . Нажмите Далее, чтобы продолжить.

Назовите проект DynamicsDemo и убедитесь, что для устройства установлено iPhone . Для этого урока я оставил поле префикса класса пустым, но не стесняйтесь вводить собственный префикс класса. Нажмите Next , сообщите Xcode, где вы хотите сохранить проект, и нажмите Create .

Загрузите исходные файлы этого руководства и добавьте изображения из проекта Xcode в свой проект в папке Images.xcassets в Навигаторе проектов .

( Изображение Copyright: icons8 )

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

Отображение и скрытие меню запускаются жестом смахивания. UIKit Dynamics будет отвечать за поведение или анимацию меню. Поведение, которое мы будем использовать:

  1. Гравитационное поведение : Гравитационное поведение заставит меню двигаться в правильном направлении, влево или вправо. Без гравитационного поведения меню не поможет.
  2. Поведение при столкновении: Поведение при столкновении одинаково важно. Без этого меню не переставало бы двигаться, если к нему применить гравитацию. Невидимая граница вызовет столкновение и заставит меню остановиться там, где мы хотим, чтобы оно остановилось.
  3. Поведение толчка . Даже если поведение гравитации и столкновения может оживлять и выводить меню, мы добавим ему дополнительное ускорение или толчок, используя поведение толчка. Это сделает анимацию более быстрой.
  4. Динамическое поведение элемента : мы также добавим динамическое поведение элемента для определения эластичности меню. Это приведет к оживлённому столкновению.

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

Давайте начнем с добавления нового класса в проект. Нажмите Command-N , выберите класс Objective C из списка шаблонов в разделе iOS> Какао Touch . Нажмите Далее, чтобы продолжить.

Назовите новый класс MenuComponent и убедитесь, что класс наследует от NSObject . Нажмите Next , сообщите Xcode, где вы хотите сохранить файлы классов, и нажмите Create .

Откройте MenuComponent.h и определите перечисление, которое будет представлять направление меню. Направление может быть слева направо или справа налево. Добавьте следующий фрагмент кода под оператором импорта.

1
2
3
4
typedef enum MenuDirectionOptionTypes{
    menuDirectionLeftToRight,
    menuDirectionRightToLeft
} MenuDirectionOptions;

Откройте MenuComponent.m , добавьте расширение класса и объявите следующие свойства:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
@interface MenuComponent()
 
@property (nonatomic, strong) UIView *menuView;
@property (nonatomic, strong) UIView *backgroundView;
@property (nonatomic, strong) UIView *targetView;
@property (nonatomic, strong) UITableView *optionsTableView;
@property (nonatomic, strong) NSArray *menuOptions;
@property (nonatomic, strong) NSArray *menuOptionImages;
@property (nonatomic, strong) UIDynamicAnimator *animator;
@property (nonatomic) MenuDirectionOptions menuDirection;
@property (nonatomic) CGRect menuFrame;
@property (nonatomic) CGRect menuInitialFrame;
@property (nonatomic) BOOL isMenuShown;
 
@end
  • menuView : это объект просмотра меню, который будет анимирован внутри и вне поля зрения. Представление таблицы будет подпредставлением этого представления.
  • backgroundView : фоновое представление является полупрозрачным видом, которое не позволяет пользователю нажимать ничего, кроме представления меню.
  • targetView : это targetView к которому будут добавлены виды меню и фона.
  • optionsTableView : это табличное представление, в котором будут перечислены опции меню.
  • menuOptions : этот массив будет содержать параметры меню, которые будут отображаться в виде таблицы.
  • menuOptionImages : этот массив будет содержать имена файлов изображений, которые находятся слева от каждой ячейки табличного представления.
  • animator : это экземпляр UIDynamicAnimator который будет анимировать меню.
  • menuDirection : это свойство имеет тип MenuDirectionOptions и определяет направление, в котором будет перемещаться меню.
  • menuFrame : это рамка меню.
  • menuInitialFrame : И это начальный кадр меню.
  • isMenuShown : флаг, указывающий, отображается меню или нет.

В дополнение к этим частным свойствам нам также нужно объявить некоторые открытые свойства. Снова зайдите в MenuComponent.h и добавьте следующий фрагмент кода:

1
2
3
4
5
6
7
8
@interface MenuComponent : NSObject
 
@property (nonatomic, strong) UIColor *menuBackgroundColor;
@property (nonatomic, strong) NSMutableDictionary *tableSettings;
@property (nonatomic) CGFloat optionCellHeight;
@property (nonatomic) CGFloat acceleration;
 
@end
  • menuBackgroundColor : это свойство используется для установки цвета фона меню.
  • tableSettings : это словарь, который я упоминал в предыдущем разделе. Это позволит нам настроить представление таблицы, задав ряд параметров.
  • optionCellHeight : это единственный атрибут табличного представления, который нельзя установить с tableSettings словаря tableSettings . Он определяет высоту строки ячеек табличного представления.
  • acceleration : это свойство определяет величину поведения нажатия, другими словами, величину силы, применяемой к представлению меню, когда оно перемещается в и из вида.

На этом этапе мы объявляем пользовательский инициализатор, в котором мы устанавливаем:

  • последний кадр представления меню, то есть кадр, когда анимация завершена и меню отображается
  • целевой вид, к которому вид меню будет добавлен как подпредставление
  • массив опций и изображений, отображаемых в виде таблицы
  • направление анимации при отображении меню

Интерфейс пользовательского инициализатора показан ниже. Добавьте его в открытый интерфейс класса MenuComponent .

1
— (id)initMenuWithFrame:(CGRect)frame targetView:(UIView *)targetView direction:(MenuDirectionOptions)direction options:(NSArray *)options optionImages:(NSArray *)optionImages;

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

01
02
03
04
05
06
07
08
09
10
11
— (id)initMenuWithFrame:(CGRect)frame targetView:(UIView *)targetView direction:(MenuDirectionOptions)direction options:(NSArray *)options optionImages:(NSArray *)optionImages {
    if (self = [super init]) {
        self.menuFrame = frame;
        self.targetView = targetView;
        self.menuDirection = direction;
        self.menuOptions = options;
        self.menuOptionImages = optionImages;
    }
     
    return self;
}

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

01
02
03
04
05
06
07
08
09
10
11
@interface MenuComponent()
 
 
— (void)setupMenuView;
— (void)setupBackgroundView;
— (void)setupOptionsTableView;
— (void)setInitialTableViewSettings;
— (void)setupSwipeGestureRecognizer;
 
@end

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
— (void)setupMenuView {
    if (self.menuDirection == menuDirectionLeftToRight) {
        self.menuInitialFrame = CGRectMake(-self.menuFrame.size.width,
                                           self.menuFrame.origin.y,
                                           self.menuFrame.size.width,
                                           self.menuFrame.size.height);
    }
    else{
        self.menuInitialFrame = CGRectMake(self.targetView.frame.size.width,
                                           self.menuFrame.origin.y,
                                           self.menuFrame.size.width,
                                           self.menuFrame.size.height);
    }
     
    self.menuView = [[UIView alloc] initWithFrame:self.menuInitialFrame];
    [self.menuView setBackgroundColor:[UIColor colorWithRed:0.0 green:0.47 blue:0.39 alpha:1.0]];
    [self.targetView addSubview:self.menuView];
}

В 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];
}

В setupOptionsTableView мы начинаем с инициализации таблицы, вызывая initWithFrame: в котором размер представления меню используется для размера представления таблицы. Экземпляр MenuComponent устанавливается как источник данных и делегат табличного представления.

01
02
03
04
05
06
07
08
09
10
— (void)setupOptionsTableView {
    self.optionsTableView = [[UITableView alloc] initWithFrame:CGRectMake(0.0, 0.0, self.menuFrame.size.width, self.menuFrame.size.height) style:UITableViewStylePlain];
    [self.optionsTableView setBackgroundColor:[UIColor clearColor]];
    [self.optionsTableView setScrollEnabled:NO];
    [self.optionsTableView setSeparatorStyle:UITableViewCellSeparatorStyleNone];
    [self.menuView addSubview:self.optionsTableView];
     
    [self.optionsTableView setDelegate:self];
    [self.optionsTableView setDataSource:self];
}

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

1
@interface MenuComponent : NSObject <UITableViewDataSource, UITableViewDelegate>

В setupInitialTableViewSettings мы инициализируем и заполняем словарь tableSettings который мы объявили ранее, в открытом интерфейсе класса.

1
2
3
4
5
6
7
8
— (void)setInitialTableViewSettings {
    self.tableSettings = [[NSMutableDictionary alloc] initWithObjectsAndKeys:
                          [UIFont fontWithName:@»American Typewriter» size:15.0], @»font»,
                          [NSNumber numberWithInt:NSTextAlignmentLeft], @»textAlignment»,
                          [UIColor whiteColor], @»textColor»,
                          [NSNumber numberWithInt:UITableViewCellSelectionStyleGray], @»selectionStyle»,
                          nil];
}

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

01
02
03
04
05
06
07
08
09
10
— (void)setupSwipeGestureRecognizer {
    UISwipeGestureRecognizer *hideMenuGesture = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(hideMenuWithGesture:)];
    if (self.menuDirection == menuDirectionLeftToRight) {
        hideMenuGesture.direction = UISwipeGestureRecognizerDirectionLeft;
    }
    else{
        hideMenuGesture.direction = UISwipeGestureRecognizerDirectionRight;
    }
    [self.menuView addGestureRecognizer:hideMenuGesture];
}

Стоит отметить две вещи. Во-первых, направление жеста смахивания зависит от значения свойства menuDirection . Во-вторых, hideMenuWithGesture: метод — это закрытый метод, который вызывается каждый раз, когда распознаватель жестов обнаруживает жест смахивания. Мы реализуем этот метод позже, но, чтобы избавиться от предупреждения компилятора, объявим метод в расширении частного класса класса MenuComponent как показано ниже.

1
2
3
4
5
6
7
@interface MenuComponent()
 
 
— (void)hideMenuWithGesture:(UISwipeGestureRecognizer *)gesture;
 
@end

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
— (id)initMenuWithFrame:(CGRect)frame targetView:(UIView *)targetView direction:(MenuDirectionOptions)direction options:(NSArray *)options optionImages:(NSArray *)optionImages {
    if (self = [super init]) {
        …
         
        // Setup the background view.
        [self setupBackgroundView];
         
        // Setup the menu view.
        [self setupMenuView];
         
        // Setup the options table view.
        [self setupOptionsTableView];
         
        // Set the initial table view settings.
        [self setInitialTableViewSettings];
         
        // Setup the swipe gesture recognizer.
        [self setupSwipeGestureRecognizer];
    }
     
    return self;
}

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
— (id)initMenuWithFrame:(CGRect)frame targetView:(UIView *)targetView direction:(MenuDirectionOptions)direction options:(NSArray *)options optionImages:(NSArray *)optionImages {
    if (self = [super init]) {
        …
         
        // Initialize the animator.
        self.animator = [[UIDynamicAnimator alloc] initWithReferenceView:self.targetView];
         
        // Set the initial height for each cell row.
        self.optionCellHeight = 50.0;
         
        // Set the initial acceleration value (push magnitude).
        self.acceleration = 15.0;
         
        // Indicate that initially the menu is not shown.
        self.isMenuShown = NO;
    }
     
    return self;
}

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

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

1
2
3
4
5
6
7
@interface MenuComponent()
 
 
— (void)toggleMenu;
 
@end

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

  • Гравитационное поведение обладает свойством direction , структурой CGVector которая определяет направление силы тяжести. Например, установив для свойства direction значение { 1.0, 0.0 } гравитационное поведение смещается вправо, тогда как значение { 0.0, 1.0 } приводит к силе, которая тянет вниз.
  • Поведение при столкновении работает либо между двумя динамическими элементами, либо между динамическим элементом и границей. В нашем примере нам нужна невидимая граница, определяемая двумя точками, которая останавливает представление меню.
  • Поведение толчка имеет свойство magnitude определяющее ускорение, применяемое к динамическому элементу. Значение также определяет направление толчка.

В методе toggleMenu мы сначала вычисляем значения вышеупомянутых свойств. Затем мы создаем и настраиваем динамические поведения и добавляем их в объект динамического аниматора.

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

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

Давайте начнем реализовывать метод toggleMenu .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
— (void)toggleMenu{
    // Remove any previous behaviors added to the animator.
    [self.animator removeAllBehaviors];
     
    // The following variables will define the direction of the menu view animation.
     
    // This variable indicates the gravity direction.
    CGFloat gravityDirectionX;
     
    // These two points specify an invisible boundary where the menu view should collide.
    // The boundary must be always to the side of the gravity direction so as the menu view
    // can stop moving.
    CGPoint collisionPointFrom, collisionPointTo;
     
    // The higher the push magnitude value, the greater the acceleration of the menu view.
    // If that value is set to 0.0, then only the gravity force will be applied to the
    // menu view.
    CGFloat pushMagnitude = self.acceleration;
}

gravityDirectionX , collisionPointFrom , collisionPointTo и pushMagnitude будут содержать значения, которые мы назначим для динамического поведения позже. Давайте установим их значения в зависимости от состояния меню и направления анимации.

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
— (void)toggleMenu{
    …
     
    // Check if the menu is shown or not.
    if (!self.isMenuShown) {
        // If the menu view is hidden and it’s about to be shown, then specify each variable
        // value depending on the animation direction.
        if (self.menuDirection == menuDirectionLeftToRight) {
            // The value 1.0 means that gravity «moves» the view towards the right side.
            gravityDirectionX = 1.0;
             
            // The From and To points define an invisible boundary, where the X-origin point
            // equals to the desired X-origin point that the menu view should collide, and the
            // Y-origin points specify the highest and lowest point of the boundary.
             
            // If the menu view is being shown from left to right, then the collision boundary
            // should be defined so as to be at the right of the initial menu view position.
            collisionPointFrom = CGPointMake(self.menuFrame.size.width, self.menuFrame.origin.y);
            collisionPointTo = CGPointMake(self.menuFrame.size.width, self.menuFrame.size.height);
        }
        else{
            // The value -1.0 means that gravity «pulls» the view towards the left side.
            gravityDirectionX = -1.0;
             
            // If the menu view is being shown from right to left, then the collision boundary
            // should be defined so as to be at the left of the initial menu view position.
            collisionPointFrom = CGPointMake(self.targetView.frame.size.width — self.menuFrame.size.width, self.menuFrame.origin.y);
            collisionPointTo = CGPointMake(self.targetView.frame.size.width — self.menuFrame.size.width, self.menuFrame.size.height);
             
            // Set to the pushMagnitude variable the opposite value.
            pushMagnitude = (-1) * pushMagnitude;
        }
         
        // Make the background view semi-transparent.
        [self.backgroundView setAlpha:0.25];
    }
    else{
        // In case the menu is about to be hidden, then the exact opposite values should be
        // set to all variables for succeeding the opposite animation.
         
        if (self.menuDirection == menuDirectionLeftToRight) {
            gravityDirectionX = -1.0;
            collisionPointFrom = CGPointMake(-self.menuFrame.size.width, self.menuFrame.origin.y);
            collisionPointTo = CGPointMake(-self.menuFrame.size.width, self.menuFrame.size.height);
             
            // Set to the pushMagnitude variable the opposite value.
            pushMagnitude = (-1) * pushMagnitude;
        }
        else{
            gravityDirectionX = 1.0;
            collisionPointFrom = CGPointMake(self.targetView.frame.size.width + self.menuFrame.size.width, self.menuFrame.origin.y);
            collisionPointTo = CGPointMake(self.targetView.frame.size.width + self.menuFrame.size.width, self.menuFrame.size.height);
        }
         
        // Make the background view fully transparent.
        [self.backgroundView setAlpha:0.0];
    }
}

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

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

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

Мы используем переменную gravityDirectionX чтобы установить свойство gravityDirection .

1
2
3
4
5
UICollisionBehavior *collisionBehavior = [[UICollisionBehavior alloc] initWithItems:@[self.menuView]];
[collisionBehavior addBoundaryWithIdentifier:@»collisionBoundary»
                                       fromPoint:collisionPointFrom
                                         toPoint:collisionPointTo];
[self.animator addBehavior:collisionBehavior];

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

Прежде чем добавить поведение push к динамическому аниматору, давайте сначала создадим поведение динамического элемента , проще говоря, универсальное поведение, которое мы будем использовать для настройки эластичности столкновения. Чем больше значение elasticity , тем выше вероятность столкновения между видом и невидимой границей. Допустимые значения варьируются от 0.0 до 1.0 .

1
2
3
UIDynamicItemBehavior *itemBehavior = [[UIDynamicItemBehavior alloc] initWithItems:@[self.menuView]];
[itemBehavior setElasticity:0.35];
[self.animator addBehavior:itemBehavior];

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

Давайте теперь добавим поведение push, которое ускорит меню.

1
2
3
UIPushBehavior *pushBehavior = [[UIPushBehavior alloc] initWithItems:@[self.menuView] mode:UIPushBehaviorModeInstantaneous];
[pushBehavior setMagnitude:pushMagnitude];
[self.animator addBehavior:pushBehavior];

При установке mode в UIPushBehaviorModeInstantaneous сила поведения толчка применяется сразу, а не постепенно. Динамическое поведение теперь добавлено в динамический аниматор, а это значит, что пришло время заставить вид меню появляться и исчезать.

Чтобы показать и скрыть меню, нам нужно вызвать закрытый метод toggleMenu . Меню должно появиться, когда жест смахивания обнаружен целевым видом в направлении смахивания. Давайте начнем с объявления публичного метода, который мы можем вызвать, чтобы показать меню. Откройте ViewController.h и объявите метод showMenu как показано ниже.

1
2
3
4
5
6
7
@interface MenuComponent : NSObject <UITableViewDataSource, UITableViewDelegate>
 
 
— (void)showMenu;
 
@end

Реализация очень проста, потому что все, что мы делаем, это toggleMenu метод toggleMenu . Обратите внимание, что мы обновляем isMenuShown чтобы отразить новое состояние меню.

1
2
3
4
5
6
7
— (void)showMenu {
    if (!self.isMenuShown) {
        [self toggleMenu];
         
        self.isMenuShown = YES;
    }
}

Давайте начнем использовать класс MenuComponent в нашем контроллере представления. Откройте ViewController.m и добавьте оператор импорта вверху.

1
#import «MenuComponent.h»

В ViewController частного класса menuComponent свойство MenuComponent типа MenuComponent для меню.

1
2
3
4
5
@interface ViewController ()
 
@property (nonatomic, strong) MenuComponent *menuComponent;
 
@end

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

1
2
3
4
5
6
7
8
9
— (void)viewDidLoad {
    [super viewDidLoad];
     
    UISwipeGestureRecognizer *showMenuGesture = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(showMenu:)];
     
    showMenuGesture.direction = UISwipeGestureRecognizerDirectionLeft;
     
    [self.view addGestureRecognizer:showMenuGesture];
}

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

После инициализации распознавателя жестов пришло время создать меню с помощью инициализатора, который мы реализовали ранее в классе MenuComponent . Добавьте следующий фрагмент кода в метод viewDidLoad контроллера представления.

1
2
3
4
5
6
CGRect desiredMenuFrame = CGRectMake(0.0, 20.0, 150.0, self.view.frame.size.height);
   self.menuComponent = [[MenuComponent alloc] initMenuWithFrame:desiredMenuFrame
                                                      targetView:self.view
                                                       direction:menuDirectionRightToLeft
                                                         options:@[@»Download», @»Upload», @»E-mail», @»Settings», @»About»]
                                                    optionImages:@[@»download», @»upload», @»email», @»settings», @»info»]];

Убедитесь, что направление, которое вы передаете в методе инициализации, совпадает с направлением распознавателя жестов.

Затем объявите showMenu: метод в расширении класса контроллера представления.

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

Реализация метода showMenu: не может быть проще.

1
2
3
— (void)showMenu:(UIGestureRecognizer *)gestureRecognizer{
    [self.menuComponent showMenu];
}

Прежде чем запускать приложение в первый раз, вернитесь в MenuComponent.m и закомментируйте вызов метода setupOptionsTableView методе инициализации. Это необходимо, если мы хотим предотвратить сбой нашего приложения, потому что мы еще не реализовали протоколы табличного представления.

1
2
// Setup the options table view.
// [self setupOptionsTableView];

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

Даже если жест отображения для отображения меню выполняется в представлении, к которому добавлено меню, жест для скрытия меню должен быть обнаружен самим меню. Вы помните, что мы создали распознаватель жестов в классе MenuComponent и объявили hideMenuWithGesture: Последний метод будет вызываться, когда необходимо скрыть меню.

Давайте реализуем hideMenuWithGesture: в MenuComponent.m . Его реализация довольно проста, как вы можете видеть ниже.

1
2
3
4
5
6
7
— (void)hideMenuWithGesture:(UISwipeGestureRecognizer *)gesture {
    // Make a call to toggleMenu method for hiding the menu.
    [self toggleMenu];
     
    // Indicate that the menu is not shown.
    self.isMenuShown = NO;
}

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

Представление меню теперь может появляться и исчезать. Пришло время сосредоточить наше внимание на настройке представления таблицы параметров и отображении параметров меню. Мы уже объявили несколько опций во время инициализации меню в методе viewDidLoad контроллера представления. Давайте посмотрим, что нам нужно сделать, чтобы отобразить их.

Первое, что нам нужно сделать, — это реализовать требуемый метод протоколов UITableViewDataSource и UITableViewDelegate . Обратите внимание, что высота ячейки определяется свойством optionCellHeight .

01
02
03
04
05
06
07
08
09
10
11
12
13
— (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return 1;
}
 
 
— (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return [self.menuOptions count];
}
 
 
— (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    return self.optionCellHeight;
}

В tableView:cellForRowAtIndexPath: мы настраиваем ячейки для отображения параметров меню и изображений. Обратите внимание, что мы используем объект tableSettings для настройки ячеек табличного представления.

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
— (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@»optionCell»];
     
    if (cell == nil) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@»optionCell»];
    }
     
    // Set the selection style.
    [cell setSelectionStyle:[[self.tableSettings objectForKey:@»selectionStyle»] intValue]];
     
    // Set the cell’s text and specify various properties of it.
    cell.textLabel.text = [self.menuOptions objectAtIndex:indexPath.row];
    [cell.textLabel setFont:[self.tableSettings objectForKey:@"font"]];
    [cell.textLabel setTextAlignment:[[self.tableSettings objectForKey:@"textAlignment"] intValue]];
    [cell.textLabel setTextColor:[self.tableSettings objectForKey:@"textColor"]];
     
    // If the menu option images array is not nil, then set the cell image.
    if (self.menuOptionImages != nil) {
        [cell.imageView setImage:[UIImage imageNamed:[self.menuOptionImages objectAtIndex:indexPath.row]]];
        [cell.imageView setTintColor:[UIColor whiteColor]];
    }
     
     
    [cell setBackgroundColor:[UIColor clearColor]];
     
    return cell;
}

Перед запуском приложения обязательно раскомментируйте вызов setupOptionsTableView.

1
2
// Setup the options table view.
[self setupOptionsTableView];

Вот и все. Запустите приложение и проведите пальцем справа налево, чтобы отобразить меню и его параметры.

Как я упоминал ранее, мы не будем реализовывать протокол делегата для меню опций. Вместо этого мы будем использовать блоки. Мы не будем добавлять какие-либо новые методы, хотя. Вместо этого мы собираемся немного изменить showMenuметод. Начните с обновления своего объявления в MenuComponent.h, как показано ниже.

1
- (void)showMenuWithSelectionHandler:(void(^)(NSInteger selectedOptionIndex))handler;

Теперь showMenu:метод принимает один аргумент — блок. Блок также принимает один аргумент — выбор пользователя.

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

1
2
3
4
5
6
@interface MenuComponent()
 
@property (nonatomic, strong) void(^selectionHandler)(NSInteger);
 
@end

Затем нам нужно обновить showMenu:метод, как показано ниже.

1
2
3
4
5
6
7
8
9
- (void)showMenuWithSelectionHandler:(void (^)(NSInteger))handler {
    if (!self.isMenuShown) {
        self.selectionHandler = handler;
         
        [self toggleMenu];
         
        self.isMenuShown = YES;
    }
}

Начнем с хранения handlerв selectionHandlerсобственности. Всякий раз, когда пользователь выбирает элемент из табличного представления, мы вызываем обработчик выбора. Посмотрите на реализацию tableView:didSelectRowAtIndexPath:для уточнения.

1
2
3
4
5
6
7
— (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    [[tableView cellForRowAtIndexPath:indexPath] setSelected:NO];
     
    if (self.selectionHandler) {
        self.selectionHandler(indexPath.row);
    }
}

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

01
02
03
04
05
06
07
08
09
10
- (void)showMenu:(UIGestureRecognizer *)gestureRecognizer {
    [self.menuComponent showMenuWithSelectionHandler:^(NSInteger selectedOptionIndex) {
        UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"UIKit Dynamics Menu"
                                                        message:[NSString stringWithFormat:@"You selected option #%d", selectedOptionIndex + 1]
                                                       delegate:nil
                                              cancelButtonTitle:nil
                                              otherButtonTitles:@"Okay", nil];
        [alert show];
    }];
}

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

В этом уроке мы создали повторно используемый компонент для отображения меню с помощью UIKit Dynamics. Если UIKit Dynamics был для вас новым, то я надеюсь, что этот урок даст вам представление о том, что вы можете с ним сделать. Не стесняйтесь поиграть с демо-приложением, чтобы лучше познакомиться с UIKit Dynamics.

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