Как создать анимацию колебания с помощью UIKit

Решил написать статью и поделиться опытом создания микровзаимодействий с простыми компонентами, такими как кнопки и ячейки UICollectionView

Изначально задача по созданию этих микровзаимодействий, реализованных в приложении Macro Challenge  —  OTIN  —  была сложным испытанием для команды проектирования, но техспециалисты с ней справились. 

Ключевые слова

  • UIView Animate;
  • CGAffineTransform;
  • CAKeyFrameAnimation.

Что такое микровзаимодействия?

Микровзаимодействия  —  это небольшие, но функциональные анимации. Они сообщают пользователю информацию о текущем состоянии и работе компонента внутри UI. К ним относятся такие действия, как анимации загрузки и сжатие кнопки при нажатии. 

Рассмотрим пример микровзаимодействий: 

Пример иллюстрирует анимацию режима редактирования по умолчанию. Она запускается, когда для tableView.isEditing устанавливается значение true. Эта анимация сжимает ячейку справа и слева, тем самым открывая с обеих сторон возможности для взаимодействия. В данном случае предлагаются такие действия, как удаление и переупорядочивание. 

На создание следующей анимации качания вдохновила предустановленная анимация iOS/iPadOS, когда главный экран находится в режиме редактирования:

Заканчиваем с теорией и переходим к практической части. Рассмотрим ряд анимаций и научимся их создавать. 

Анимация режима редактирования UITableView 

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

Создаем кнопку для управления переключателем состояния редактирования: 

@objc private func editButtonAction() {
tableView.setEditing(!tableView.isEditing, animated: true)
// Можно также использовать функцию .toggle()
}

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

Вы можете также поэкспериментировать с анимациями для каждой ячейки в процессе удаления через tableView.deleteRows(...) и вставки через tableView.insertRows(...):

extension ParentRoutineViewController: UITableViewDelegate, UITableViewDataSource {
...
// Определение стиля редактирования для каждой ячейки
func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle {
guard tableView.isEditing else { return .none }
return .delete
}

func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {

if editingStyle == .delete {
// Что происходит после нажатия на delete?
tableView.deleteRows(at: [indexPath], with: .left)
}
}
}

extension ParentRoutineViewController: UITableViewDragDelegate {
func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
// Что делать при перемещении?
}

func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
// Что делать после сброса?
}
}

Анимация UICollectionViewCell

Создание анимации колебания для UICollectionViewCell происходит с помощью CAKeyframeAnimation из-за необходимости контролировать амплитуду колебания при каждом движении анимации: 

// Создание анимации колебания
let wobble = CAKeyframeAnimation(keyPath: "transform.rotation")
wobble.values = [0.0, -0.025, 0.0, 0.025, 0.0]
wobble.keyTimes = [0.0, 0.25, 0.5, 0.75, 1.0]
wobble.duration = 0.4
wobble.isAdditive = true
wobble.repeatCount = Float.greatestFiniteMagnitude

// Добавление анимации колебания в каждую ячейку collectionView 
collectionView.indexPathsForVisibleItems.forEach { (indexPath) in
    let cell = parentActivityListDetailView.collectionView.cellForItem(at: indexPath) as! CustomCollectionViewCell
    cell.layer.add(wobble, forKey: "wobble")
}

Не забудьте удалить анимации по завершении редактирования: 

collectionView.indexPathsForVisibleItems.forEach { (indexPath) in
let cell = parentActivityListDetailView.collectionView.cellForItem(at: indexPath) as! CustomCollectionViewCell
cell.layer.removeAllAnimations()
}

Анимация UIButtons: сжатие и расширение 

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

Сначала создаем кнопку: либо посредством раскадровки, либо путем программирования. В данном случае создана обычная кнопка UIButton с именем button.

Задействуем CGAffineTransform, стандартную структуру для масштабирования и аффинного преобразования. Параметры scaleX и y потребуются для управления масштабированием, а параметры translationX и y  —  для управления преобразованием. 

button.addTarget(self, action: #selector(animateUp), for: .touchUpOutside)
button.addTarget(self, action: #selector(animateUp), for: .touchUpInside)
button.addTarget(self, action: #selector(animateDown), for: .touchDown)

// Устанавливаем состояние по умолчанию с масштабом 1.0 в состоянии up 
@objc func animateUp(_ sender: UIButton) {
    UIView.animate(withDuration: 0.1, animations: {
          button.transform = CGAffineTransform(sscaleX: 1.0, y: 1.0)
    }, completion: { _ in
      // Обработчик выполнения (англ. Completion Handler)
    })
}

// Уменьшение масштаба до 0.9 при нажатии 
@objc func animateDown(_ sender: UIButton) {
    UIView.animate(withDuration: 0.1, animations: {
          button.transform = CGAffineTransform(scaleX: 0.9, y: 0.9)
    }, completion: { _ in
      // Обработчик выполнения 
    })
}

Анимация UIView: ввод PIN-кода 

Клавиатура для ввода PIN-кода 

В данном проекте клавиатура для ввода PIN-кода представляет собой UICollectionView. В связи с этим для имитации состояний UIButton, таких как .touchDown.touchUpInside и  .touchUpOutside, мы должны использовать некоторые заглушки протокола самого представления коллекции. Рассмотрим код более подробно: 

extension ChildPinUnlockViewController: UICollectionViewDelegate {

// touchUpOutside
public func collectionView(_ collectionView: UICollectionView, didUnhighlightItemAt indexPath: IndexPath) {
let cell = collectionView.cellForItem(at: indexPath) ?? UICollectionViewCell()
UIView.animate(withDuration: 0.05, animations: {
button.transform = CGAffineTransform(scaleX: 1.0, y: 1.0)
}, completion: { _ in
// Обработчик выполнения
})

}

// touchDown
public func collectionView(_ collectionView: UICollectionView, didHighlightItemAt indexPath: IndexPath) {
let cell = collectionView.cellForItem(at: indexPath) ?? UICollectionViewCell()
UIView.animate(withDuration: 0.05, animations: {
button.transform = CGAffineTransform(scaleX: 0.9, y: 0.9)
}, completion: { _ in
// Обработчик выполнения
})
}

// touchUpInside
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let cell = collectionView.cellForItem(at: indexPath) ?? UICollectionViewCell()
UIView.animate(withDuration: 0.05, animations: {
button.transform = CGAffineTransform(scaleX: 1.0, y: 1.0)
}, completion: { _ in
// Обработчик выполнения
})
}

}

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

let shake = CAKeyframeAnimation(keyPath: "transform.translation.x")
shake.values = [0, 20, -20, 0]
shake.duration = 0.25
shake.repeatCount = 1
pinStackView.layer.add(shake, forKey: "shake")

На этом мы завершаем тему создания анимации колебания.

Читайте также:

Читайте нас в TelegramVK и Дзен


Перевод статьи Gregorius Albert: How to Create Wobble Animations Using UIKit

Предыдущая статьяРешение головоломки судоку на JavaScript с помощью хэш-карт и рекурсий
Следующая статьяKubernetes: установка MicroK8s на локальном компьютере за 5 минут