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. Для этого:

  1. Создайте новый класс Cocoa Touch Class, в качестве субкласса задайте UITableViewCell и отметьте галочкой пункт о создании файла XIB.
  2. Откройте 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)
            })
        }
    }
}
///перечисление для анимаций

Мы добавили перечисление, содержащее в себе четыре типа анимации. Это перечисление служит фабрикой, которая предоставляет анимацию классу-аниматору. Вот список добавленных анимаций:

  1. Анимация постепенного появления (Fade-In). Анимирует ячейки TableView на основе альфы ячейки.
  2. Анимация движения вверх (Move-Up). Анимирует ячейки TableView на основе их положения.
  3. Анимация движения вверх с постепенным появлением (Move-Up-Fade). Анимирует ячейки TableView на основе альфы ячейки и ее положения одновременно. 
  4. Анимация движения вверх с подпрыгиванием (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

Ресурсы

  1. Репозиторий Github.
  2. Документация разработчика Apple по анимации.

Заключение

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

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

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

Читайте нас в Telegram, VK и Яндекс.Дзен


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

Предыдущая статьяБезопасность наглядно: CORS
Следующая статьяНаглядное объяснение алгоритма Беллмана-Форда