Анимируем скучные табличные представления в iOS-приложении


Почти в любом iOS-приложении присутствуют табличные представления. Их применяют для отображения одиночного столбца содержимого с вертикальной прокруткой, разделённого на строки.

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

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

Я долго сопротивлялся, прежде чем начать работать с UIView-анимацией: оправдывался тем, что это чересчур сложно, в этом нет необходимости, а на изучение уйдет много времени. Позже я прочитал несколько блестящих блогов и книги по UX/UI и осознал, насколько могущественным инструментом может быть анимация. После всего этого несложно было убедить себя и приступить к её освоению. 

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

Анимации TableView

Предусловия

От вас потребуются некоторое представление об iOS-разработке, но даже если её понятия не вполне вам знакомы, не стесняйтесь читать до конца  —  я объясняю всё простым языком. Всё, что вам нужно, — это Macbook, Xcode, а также немного знаний об Auto Layouts, методе UIView.animate и перечислениях (enum).

Введение

Ознакомимся с представлением, созданию которого посвящено это руководство. Наше представление будет состоять из TableView и горизонтального StackView с четырьмя кнопками на равном расстоянии друг от друга. Этими кнопками мы будем изменять TableViewHeader и анимацию ячеек в табличном представлении.

Руководство

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

Создание UI

Откройте Main.storyboard и настройте представление, как показано на картинке:

Добавьте компоненты в сцену ViewController в Main.Storyboard

Вот, что мы добавили:

  • TableView  —  ограничения top, leading и trailing установлены в 0, а пропорциональная высота на 77% от всего представления;
  • горизонтальный StackView — ограничения top, leading и trailing установлены на 24;
  • кнопки  —  ограничение по высоте 42, все четыре кнопки размещены внутри stackView.

У каждой кнопки должен быть ассоциированный с ней тэг. Добавьте его через инспектор атрибутов:

  • Button1  —  тэг 1;
  • Button2  —  тэг 2;
  • Button3  —  тэг 3;
  • Button4  —  тэг 4.

Далее создадим пользовательский класс TableViewClassс пользовательским XIB. Для этого:

  • Создайте новый класс Cocoa Touch Class, в качестве субкласса задайте UITableViewCell и отметьте галочкой пункт о создании файла XIB.
  • Откройте XIB-файл, относящийся к UITableViewCell.
  • XIB пользовательского UITableView

    Для этой ячейки мы добавили одиночный 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.

    • Button1 применяет к TableViewCells анимацию Fade-In.
    • Button2 применяет к TableViewCells анимацию Move-Up.
    • Button3 применяет к TableViewCells анимацию Move-Up-Fade.
    • Button4 применяет к TableViewCells анимацию Move-Up-Bounce.

    Вот и всё! Теперь при запуске приложения вы увидите на TableView очаровательную анимацию, которая изменяется по нажатию на разные кнопки. И вот он, конечный результат. Нажимаем на кнопки  —  и табличное представление анимируется.

    Готовая анимация TableView

    Ресурсы

  • Репозиторий Github.
  • Документация разработчика Apple по анимации.
  • Заключение

    Чему мы научились?

  • Мы начали с того, что добавили в файлMain.storyboard необходимые для анимации TableView компоненты.
  • Затем мы создали пользовательский UITableViewClass и добавили компоненты в его XIB-файл.
  • Создали класс TableViewAnimator, который анимирует TableView (помните об отдельном классе для каждого анимирования). Мы также создали перечисление TableAnimationFactory, внутри которого определили четыре анимации.
  • Мы создали новое перечисление TableAnimation, чтобы путем создания функций соединить названия и анимации, образовав варианты.
  • В конечном счете, мы написали код для ViewController, настроили TableView, добавили функцию точек выхода для кнопок и назначили анимации для TableView. Спасибо, что прочитали и до скорых встреч!

  • Перевод статьи : Shubham Singh, Animate the Boring TableViews in Your iOS App


    Поделиться статьей:


    Вернуться к статьям

    Комментарии

      Ничего не найдено.