Почти в любом iOS-приложении присутствуют табличные представления. Их применяют для отображения одиночного столбца содержимого с вертикальной прокруткой, разделённого на строки.
Анимация — отличный способ привлечь внимание пользователя, а заодно сделать приложение визуально мягче. Анимировать ячейки табличного представления мы будем с помощью метода UIView.animate
класса UIView
. Вот как описывается этот метод:
Анимация изменяется в одном или нескольких представлениях с заданной длительностью. Во время анимации взаимодействие анимируемых представлений с пользователем прекращается.
Я долго сопротивлялся, прежде чем начать работать с UIView-анимацией: оправдывался тем, что это чересчур сложно, в этом нет необходимости, а на изучение уйдет много времени. Позже я прочитал несколько блестящих блогов и книги по UX/UI и осознал, насколько могущественным инструментом может быть анимация. После всего этого несложно было убедить себя и приступить к её освоению.
Что же, добавим анимации четырех типов. Вот так будут выглядеть с ними ячейки табличного представления:
Анимации TableViewОт вас потребуются некоторое представление об iOS-разработке, но даже если её понятия не вполне вам знакомы, не стесняйтесь читать до конца — я объясняю всё простым языком. Всё, что вам нужно, — это Macbook, Xcode, а также немного знаний об Auto Layouts, методе UIView.animate
и перечислениях (enum
).
Ознакомимся с представлением, созданию которого посвящено это руководство. Наше представление будет состоять из TableView
и горизонтального StackView
с четырьмя кнопками на равном расстоянии друг от друга. Этими кнопками мы будем изменять TableViewHeader
и анимацию ячеек в табличном представлении.
Для начала создадим одностраничное приложение в Xcode и выберем в качестве шаблона пользовательского интерфейса Storyboards
.
Откройте Main.storyboard
и настройте представление, как показано на картинке:
Вот, что мы добавили:
TableView
— ограничения top
, leading
и trailing
установлены в 0
, а пропорциональная высота на 77% от всего представления;StackView
— ограничения top
, leading
и trailing
установлены на 24
;stackView
.У каждой кнопки должен быть ассоциированный с ней тэг. Добавьте его через инспектор атрибутов:
Далее создадим пользовательский класс TableViewClass
с пользовательским XIB. Для этого:
Cocoa Touch Class
, в качестве субкласса задайте UITableViewCell
и отметьте галочкой пункт о создании файла XIB.UITableViewCell
.Для этой ячейки мы добавили одиночный containerView
внутри представления содержимого. Верхние и нижние ограничения (top
и bottom
) здесь установлены на 5, а ведущие и конечные ограничения (leading
и trailing
) — на 12.
Сначала добавим код в класс UITableViewCell
.
import UIKit
class TableAnimationViewCell: UITableViewCell {
override class func description() -> String {
return "TableAnimationViewCell"
}
// Отметка:- точки выхода для viewController
@IBOutlet weak var containerView: UIView!
// свойства для tableViewCell
var tableViewHeight: CGFloat = 62
var color = UIColor.white {
didSet {
self.containerView.backgroundColor = color
}
}
override func awakeFromNib() {
super.awakeFromNib()
self.selectionStyle = .none
self.containerView.layer.cornerRadius = 4
}
}
Класс UITableViewCell
устроен очень просто.
Мы добавляем точку выхода для containerView
и задаём две переменные:
TableViewHeight
— высота ячейки при доступе из viewController
;Color
— цвет ячейки.Также мы добавили код, который задает в качестве стиля none
и закругляет края ячейки.
Далее создадим класс TableViewAnimator
и добавим его в ранее созданный класс, чтобы анимировать TableView
.
Рекомендую всегда создавать отдельный класс для анимирования так, чтобы он был универсальным и его можно было применить к любым представлениям. В создании пользовательского класса есть и еще одно преимущество: он сохраняет богатые возможности настройки, так что в блоке инициализации можно предоставлять желаемые эффект и продолжительность анимации.
import UIKit
// определяем псевдоним типа для удобства применения
typealias TableCellAnimation = (UITableViewCell, IndexPath, UITableView) -> Void
// класс для анимации, которая применяется к tableView
final class TableViewAnimator {
private let animation: TableCellAnimation
init(animation: @escaping TableCellAnimation) {
self.animation = animation
}
func animate(cell: UITableViewCell, at indexPath: IndexPath, in tableView: UITableView) {
animation(cell, indexPath, tableView)
}
}
Далее создадим перечисление TableAnimationFactory
и добавим четыре метода анимации:
///перечисление для анимаций
tableViewCell
enum TableAnimationFactory {
/// проявляет ячейку, устанавливая канал прозрачности (альфа-канал) на 0, а следом анимирует ячейки по альфе, основываясь на indexPaths
static func makeFadeAnimation(duration: TimeInterval, delayFactor: TimeInterval) -> TableCellAnimation {
return { cell, indexPath, _ in
cell.alpha = 0
UIView.animate(
withDuration: duration,
delay: delayFactor * Double(indexPath.row),
animations: {
cell.alpha = 1
})
}
}
/// проявляет ячейку, устанавливая канал прозрачности на 0 и сдвигает ячейку вниз, затем анимирует ячейку по альфе и возвращает к первоначальной позиции, основываясь на indexPaths
static func makeMoveUpWithFadeAnimation(rowHeight: CGFloat, duration: TimeInterval, delayFactor: TimeInterval) -> TableCellAnimation {
return { cell, indexPath, _ in
cell.transform = CGAffineTransform(translationX: 0, y: rowHeight * 1.4)
cell.alpha = 0
UIView.animate(
withDuration: duration,
delay: delayFactor * Double(indexPath.row),
options: [.curveEaseInOut],
animations: {
cell.transform = CGAffineTransform(translationX: 0, y: 0)
cell.alpha = 1
})
}
}
/// сдвигает ячейку вниз, затем анимирует ячейку и возвращает ее к первоначальной позиции, основываясь на indexPaths
static func makeMoveUpAnimation(rowHeight: CGFloat, duration: TimeInterval, delayFactor: TimeInterval) -> TableCellAnimation {
return { cell, indexPath, _ in
cell.transform = CGAffineTransform(translationX: 0, y: rowHeight * 1.4)
UIView.animate(
withDuration: duration,
delay: delayFactor * Double(indexPath.row),
options: [.curveEaseInOut],
animations: {
cell.transform = CGAffineTransform(translationX: 0, y: 0)
})
}
}
///сдвигает ячейку вниз, затем анимирует ячейку и возвращает ее к первоначальной позиции с пружинным подпрыгиванием, основываясь на indexPaths
static func makeMoveUpBounceAnimation(rowHeight: CGFloat, duration: TimeInterval, delayFactor: Double) -> TableCellAnimation {
return { cell, indexPath, tableView in
cell.transform = CGAffineTransform(translationX: 0, y: rowHeight)
UIView.animate(
withDuration: duration,
delay: delayFactor * Double(indexPath.row),
usingSpringWithDamping: 0.6,
initialSpringVelocity: 0.1,
options: [.curveEaseInOut],
animations: {
cell.transform = CGAffineTransform(translationX: 0, y: 0)
})
}
}
}
///перечисление для анимаций
Мы добавили перечисление, содержащее в себе четыре типа анимации. Это перечисление служит фабрикой, которая предоставляет анимацию классу-аниматору. Вот список добавленных анимаций:
Fade-In
). Анимирует ячейки TableView
на основе альфы ячейки.Move-Up
). Анимирует ячейки TableView
на основе их положения.Move-Up-Fade
). Анимирует ячейки TableView
на основе альфы ячейки и ее положения одновременно. Move-Up-Bounce
). Анимирует ячейки TableView
на основе положения ячейки с помощью анимации подпрыгивания.Далее, чтобы не загромождать ViewController
, создадим с помощью ещё одного перечисления поставщик данных, который обеспечивает получение анимаций из фабрики анимации. Также добавим каждому варианту в перечислении функцию получения заголовка tableView
.
Перечисления отлично подходят для того, чтобы сопоставить функцию каждому варианту из списка: это предохраняет код от появления ошибок. Чтобы обеспечить поступление данных в ViewController
, создадим файл Tables.swift
и перечисление TableAnimation
:
import UIKit
/// Поставщик-перечисление, необходимый, чтобы обеспечить animationTitle и получить метод анимации из фабрики
enum TableAnimation {
case fadeIn(duration: TimeInterval, delay: TimeInterval)
case moveUp(rowHeight: CGFloat, duration: TimeInterval, delay: TimeInterval)
case moveUpWithFade(rowHeight: CGFloat, duration: TimeInterval, delay: TimeInterval)
case moveUpBounce(rowHeight: CGFloat, duration: TimeInterval, delay: TimeInterval)
// обеспечивает необходимую длительность и задержку анимации в зависимости от конкретного варианта
func getAnimation() -> TableCellAnimation {
switch self {
case .fadeIn(let duration, let delay):
return TableAnimationFactory.makeFadeAnimation(duration: duration, delayFactor: delay)
case .moveUp(let rowHeight, let duration, let delay):
return TableAnimationFactory.makeMoveUpAnimation(rowHeight: rowHeight, duration: duration,
delayFactor: delay)
case .moveUpWithFade(let rowHeight, let duration, let delay):
return TableAnimationFactory.makeMoveUpWithFadeAnimation(rowHeight: rowHeight, duration: duration,
delayFactor: delay)
case .moveUpBounce(let rowHeight, let duration, let delay):
return TableAnimationFactory.makeMoveUpBounceAnimation(rowHeight: rowHeight, duration: duration,
delayFactor: delay)
}
}
// предоставляет заголовок в зависимости от варианта
func getTitle() -> String {
switch self {
case .fadeIn(_, _):
return "Fade-In Animation"
case .moveUp(_, _, _):
return "Move-Up Animation"
case .moveUpWithFade(_, _, _):
return "Move-Up-Fade Animation"
case .moveUpBounce(_, _, _):
return "Move-Up-Bounce Animation"
}
}
}
Мы определили перечисление TableAnimation
, где содержатся четыре варианта, по числу анимаций, вместе с соответствующими значениями, предоставляющими методы фабрики анимации.
Перечисление, кроме того, содержит две функции:
GetAnimation
возвращает анимацию из animationFactory
в зависимости от варианта в перечислении и связанных с ним значений.GetTitle
возвращает название анимации в зависимости от варианта в перечислении.Теперь напишем код для ViewController
. Начнем с того, что подключим во ViewController
точки выхода для представления (также нам понадобятся функции точек выхода для кнопок, но их мы определим чуть позже).
import UIKit
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
// MARK:- точки выхода для viewController
@IBOutlet weak var tableView: UITableView!
@IBOutlet weak var button1: UIButton!
@IBOutlet weak var button2: UIButton!
@IBOutlet weak var button3: UIButton!
@IBOutlet weak var button4: UIButton!
Добавим к ViewController
переменные:
// MARK:- переменные для viewController
var colors = [UIColor.systemRed, UIColor.systemBlue, UIColor.systemOrange,
UIColor.systemPurple,UIColor.systemGreen]
var tableViewHeaderText = ""
/// перечисление типа TableAnimation - определяет, какая анимация будет применена к tableViewCells
var currentTableAnimation: TableAnimation = .fadeIn(duration: 0.85, delay: 0.03) {
didSet {
self.tableViewHeaderText = currentTableAnimation.getTitle()
}
}
var animationDuration: TimeInterval = 0.85
var delay: TimeInterval = 0.05
var fontSize: CGFloat = 26
Краткое описание переменных:
Colors
— цвета в TableView
.TableViewHeaderText
— заголовок TableView
.CurrentTableAnimation
— значение типа в определённой ранее модели перечисления; устанавливает переменную TableViewHeaderText
при изменении самого перечисления.AnimationDuration
— длительность анимации ячейки.Delay
— задержка между анимацией каждой ячейки.FontSize
— размер символов на кнопках. Далее зарегистрируем TableView
и напишем метод жизненного цикла ViewController
.
// MARK:- методы жизненного цикла ViewController
override func viewDidLoad() {
super.viewDidLoad()
self.colors.append(contentsOf: colors.shuffled())
// регистрация tableView
self.tableView.register(UINib(nibName: TableAnimationViewCell.description(), bundle: nil),
forCellReuseIdentifier: TableAnimationViewCell.description())
self.tableView.delegate = self
self.tableView.dataSource = self
self.tableView.isHidden = true
// задает none в качестве значения separatorStyle и задает заголовок tableView
self.tableView.separatorStyle = .none
self.tableViewHeaderText = self.currentTableAnimation.getTitle()
// устанавливает выбранной кнопку button1 и перезагружает данные tableView для воспроизведения анимации
button1.setImage(UIImage(systemName: "1.circle.fill", withConfiguration:
UIImage.SymbolConfiguration(pointSize: fontSize, weight: .semibold, scale: .large)), for: .normal)
DispatchQueue.main.asyncAfter(deadline: .now()) {
self.tableView.isHidden = false
self.tableView.reloadData()
}
}
Мы зарегистрировали пользовательский класс UITableViewCell
с нашим TableView
, задали для viewController
делегат и источник данных и установили, что кнопка button1
будет выглядеть выбранной. Кроме того, мы асинхронно перезагружаем данные, чтобы воспроизвести анимацию. Далее определим методы делегата и источника данных для TableView
в viewController
.
// делегаты функций tableView
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return colors.count
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return TableAnimationViewCell().tableViewHeight
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = tableView.dequeueReusableCell(withIdentifier: TableAnimationViewCell.description(),
for: indexPath) as? TableAnimationViewCell {
// устанавливает цвет ячейки
cell.color = colors[indexPath.row]
return cell
}
fatalError()
}
// для отображения headerTitle в tableView
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let headerView = UIView(frame: CGRect.init(x: 0, y: 0, width: tableView.frame.width, height: 42))
headerView.backgroundColor = UIColor.systemBackground
let label = UILabel()
label.frame = CGRect(x: 24, y: 12, width: self.view.frame.width, height: 42)
label.text = tableViewHeaderText
label.textColor = UIColor.label
label.font = UIFont.systemFont(ofSize: 26, weight: .medium)
headerView.addSubview(label)
return headerView
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 72
}
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
// извлекает анимацию из перечисления TableAnimation и инициализирует класс TableViewAnimator
let animation = currentTableAnimation.getAnimation()
let animator = TableViewAnimator(animation: animation)
animator.animate(cell: cell, at: indexPath, in: tableView)
}
Мы добавили стандартные методы для инициализации TableView
. Вкратце пробежимся по ним:
numberOfRowsInSection
определяет, сколько ячеек будет отображаться в TableView
.heightForRowAt
определяет высоту ячеек TableView
.cellForRowAt
инициализирует ячейку для indexPath
, назначает ей цвет и возвращает в TableView
.viewForHeaderInSection
определяет представление для TableHeader
. Мы настроили его так, чтобы он показывал метку со значением, полученным из перечисления.heightForHeaderInSection
— высота TableHeader
.willDisplay
— самый важный метод в этом руководстве. С его помощью мы извлекаем анимацию из перечисления currentAnimation
, инициализируем с этой анимацией класс TableViewAnimator
, далее анимируем ячейку, вызывая анимирующий метод.И, наконец, добавим код для взаимодействия кнопок. Каждая кнопка должна перезагружать таблицу со значением, полученным из перечисления Table
.
// MARK:- точки выхода функций для viewController
@IBAction func animationButtonPressed(_ sender: Any) {
guard let senderButton = sender as? UIButton else { return }
/// задает незакрашенный кружок в качестве символа по умолчанию для кнопок
button1.setImage(UIImage(systemName: "1.circle", withConfiguration:
UIImage.SymbolConfiguration(pointSize: fontSize, weight: .semibold, scale: .large)),
for: .normal)
button2.setImage(UIImage(systemName: "2.circle", withConfiguration:
UIImage.SymbolConfiguration(pointSize: fontSize, weight: .semibold, scale: .large)),
for: .normal)
button3.setImage(UIImage(systemName: "3.circle", withConfiguration:
UIImage.SymbolConfiguration(pointSize: fontSize, weight: .semibold, scale: .large)),
for: .normal)
button4.setImage(UIImage(systemName: "4.circle", withConfiguration:
UIImage.SymbolConfiguration(pointSize: fontSize, weight: .semibold, scale: .large)),
for: .normal)
/// задает символ для кнопки на основе ее тэга, чтобы продемонстрировать, что она выбрана, и задает currentTableAnimation.
switch senderButton.tag {
case 1: senderButton.setImage(UIImage(systemName: "1.circle.fill", withConfiguration:
UIImage.SymbolConfiguration(pointSize: fontSize, weight: .semibold,
scale: .large)), for: .normal)
currentTableAnimation = TableAnimation.fadeIn(duration: animationDuration, delay: delay)
case 2: senderButton.setImage(UIImage(systemName: "2.circle.fill", withConfiguration:
UIImage.SymbolConfiguration(pointSize: fontSize, weight: .semibold,
scale: .large)), for: .normal)
currentTableAnimation = TableAnimation.moveUp(rowHeight: TableAnimationViewCell().tableViewHeight,
duration: animationDuration, delay: delay)
case 3: senderButton.setImage(UIImage(systemName: "3.circle.fill", withConfiguration:
UIImage.SymbolConfiguration(pointSize: fontSize, weight: .semibold,
scale: .large)), for: .normal)
currentTableAnimation = TableAnimation.moveUpWithFade(rowHeight: TableAnimationViewCell().tableViewHeight,
duration: animationDuration, delay: delay)
case 4: senderButton.setImage(UIImage(systemName: "4.circle.fill", withConfiguration:
UIImage.SymbolConfiguration(pointSize: fontSize, weight: .semibold,
scale: .large)), for: .normal)
currentTableAnimation = TableAnimation.moveUpBounce(rowHeight: TableAnimationViewCell().tableViewHeight,
duration: animationDuration + 0.2, delay: delay)
default: break
}
/// перезагружаем tableView, чтобы увидеть анимацию
self.tableView.reloadData()
}
}
Важно! Все четыре кнопки должны быть назначены на одну точку выхода. Для этого руководства я выбрал функцию animationButtonPressed
.
Поскольку нам хочется, чтобы в качестве выбранной отображалась только одна кнопка за раз, мы по умолчанию назначаем всем кнопкам символ незакрашенного кружка, снимая закрашивание с ранее выбранной кнопки.
Теперь переключение помогает “закрасить” символ кнопки, основываясь на тэге, назначенном этой кнопке. Соответственно этому выбору также устанавливается значение переменной currentTableAnimation
.
TableViewCells
анимацию Fade-In
.TableViewCells
анимацию Move-Up
.TableViewCells
анимацию Move-Up-Fade
.TableViewCells
анимацию Move-Up-Bounce
.Вот и всё! Теперь при запуске приложения вы увидите на TableView
очаровательную анимацию, которая изменяется по нажатию на разные кнопки. И вот он, конечный результат. Нажимаем на кнопки — и табличное представление анимируется.
Чему мы научились?
Main.storyboard
необходимые для анимации TableView
компоненты.UITableViewClass
и добавили компоненты в его XIB-файл.TableViewAnimator
, который анимирует TableView
(помните об отдельном классе для каждого анимирования). Мы также создали перечисление TableAnimationFactory
, внутри которого определили четыре анимации.TableAnimation
, чтобы путем создания функций соединить названия и анимации, образовав варианты.ViewController
, настроили TableView
, добавили функцию точек выхода для кнопок и назначили анимации для TableView
. Спасибо, что прочитали и до скорых встреч!Перевод статьи : Shubham Singh, Animate the Boring TableViews in Your iOS App
Комментарии