Анимации в iOS-приложениях, рождённые на сервере

px9hmgqrpebneeamcitvv8b_wkw.jpeg

Полгода назад мы представили одну из самых впечатляющих функций Badoo — прямые трансляции. Среди прочего она позволяет пользователям выразить благодарность любимым стримерам в виде подарков. Мы хотели сделать эти подарки максимально яркими и привлекательными, поэтому решили их оживить — другими словами, анимировать. А чтобы было ещё интереснее, мы планировали обновлять подарки и анимации каждые несколько недель.

iOS-инженеры наверняка догадались, о каких объёмах работы идёт речь: чтобы удалять старые и добавлять новые анимации, необходимо совершить множество действий с клиентской стороны. Для этого в каждом релизе должны быть задействованы Android- и iOS-команды, а вкупе со временем, необходимым на одобрение обновления в App Store, это означает, что запуск каждого релиза с обновлёнными анимациями может занять несколько дней. Однако нам удалось решить эту проблему, и сейчас я расскажу как.


К тому времени мы уже умели экспортировать анимации Adobe After Effects (далее — AAE) в понятный нашему iOS-приложению формат при помощи библиотеки Lottie. В этот раз мы пошли чуть дальше: решили хранить все актуальные анимации на сервере и скачивать их по мере необходимости.
wqtz6w0lgzbjm1eq5bvcshngnxw.png

Пример реальной анимации в нашем приложении, полученной таким способом:

pe3sqoahuswtkbfqd4wl-opzv8m.gif

Однако в этом посте в качестве примера я возьму простенькую анимацию, которую создал сам. Она не такая креативная, как в Badoo, но вполне подходит для демонстрации нашего подхода.


AAE-проект, который я использую, можно найти вместе с другими исходниками на GitHub. Итак, открыв проект, расположенный по адресу _raw/animations/Fancy/Fancy.aep, вы увидите окно:

_khzosjwwyiiysdwfdon6iqjz10.png

Сейчас я расскажу не о процессе создания анимаций в AAE, а о том, как импортировать уже существующие анимации из AAE в подходящий для iOS-приложения формат при помощи плагина Bodymovin.

Установив плагин, откройте его, выбрав в меню Window/Extensions/Bodymovin:

jq3drw28ghwylkqidrecb78r9p4.png

Появится окно Bodymovin, в котором можно выбрать анимацию для экспорта, папку для сохранения получившегося файла и открыть настройки экспорта:

iythylsx8lylzanrfavxmtn7u64.png

В настройках анимации мы можем попросить Bodymovin включить ресурсы в JSON-файл, выбрав пункт Assets / Include in json:

c88vodmi3cksysyvdfg-89qqwby.png

Наконец, нажатием кнопки Render экспортируем и сохраняем в файл выбранную анимированную композицию.


Предположим, что мы загрузили JSON-файлы отрендеренных анимаций на веб-сервер. В нашем случае для простоты я поместил их в репозиторий проекта на GitHub. Анимации доступны здесь:

Базовая ссылка https://raw.githubusercontent.com/chupakabr/server-provided-animations/master/_raw/rendered-animations/

Идентификаторы анимаций:

  • clouds.json
  • fireworks.json

Примечание: ищете написанный на Swift веб-сервер для анимаций? Решение доступно здесь, а подробное объяснение — в этой статье.

Итак, у нас имеется рабочий сервер с анимациями, а потому пора перейти к самой захватывающей части: рендерингу анимаций на экране.


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

Загрузка анимаций


Учитывая, что REST API для получения данных уже готов, пришло время ввести протокол поставщика данных и добавить его имплементацию, которая скачивает данные с сервера:

import Lottie

protocol AnimationsProviderProtocol {
    typealias Completion = (_ animation: LOTComposition?) -> Void

    func loadAnimation(byId id: String, completion: @escaping Completion)
}

final class ServerAnimationProvider: AnimationsProviderProtocol {
    private let endpoint: URL

    init(endpoint: URL) {
        self.endpoint = endpoint
    }

    func loadAnimation(byId id: String, completion: @escaping Completion) {
        let path = "/\(id).json"
        guard let animationUrl = URL(string: path, relativeTo: self.endpoint) else {
            completion(nil)
            return
        }

        URLSession.shared.invalidateAndCancel()

        let task = URLSession.shared.dataTask(with: animationUrl) { (data, response, error) in
            guard error == nil, let data = data, let json = self.parseJson(from: data) else {
                completion(nil)
                return
            }
            let animation = LOTComposition(json: json)
            completion(animation)
        }
        task.resume()
    }

    private func parseJson(from data: Data?) -> [AnyHashable : Any]? {
        guard let data = data else { return nil }

        do {
            let json = try JSONSerialization.jsonObject(with: data, options: []) as? [AnyHashable : Any]
            return json
        } catch {
            return nil
        }
    }
}

Этот класс поставщика данных позволяет нам по запросу загружать с сервера анимации в формате JSON и хранить их в памяти для отрисовки на UI. Предположим, что мы следуем паттерну MVVM, — тогда его легко использовать в сущности ViewModel следующим образом:

 // ...
  private let animationProvider: AnimationsProviderProtocol
  private(set) var animationModel: LOTComposition?
  // …
  func loadAnimation(byId animationId: String) {
      self.animationProvider.loadAnimation(byId: animationId) { [weak self] (animationModel) in
          self?.animationModel = animationModel
      }
  }
  // ...

ViewModel обновляет свойство выбранной анимации при получении корректного HTTP-ответа от сервера с непустым JSON-объектом. Эти данные используются слоем представления для отображения анимации.

Слой представления


Теперь мы можем использовать ViewModel для получения доступа к данным анимации и отображать их на UI при помощи встроенного обработчика действия on tap, привязанного к кнопке:

class ViewController: UIViewController {
    // ...
    @IBOutlet weak var animationContainer: UIView!

    override func viewDidLoad() {
        super.viewDidLoad()
        // ...
        self.animationView = {
            let view = LOTAnimationView(frame: self.animationContainer.bounds)
            self.animationContainer.addSubview(view)
            return view
        }()
    }

    @IBAction func onPlayAnimationAction(_ sender: Any) {
        self.animationView.stop()

        self.animationView.sceneModel = self.viewModel.animationModel
        self.animationView.play()
    }
}

При нажатии на кнопку экземпляр LOTAnimationView обновляется с помощью свежих данных из ViewModel.

Вот как это выглядит:

rn3dqqo6ueg3tfye-tmh9rlhove.png

Вот и всё. Теперь в приложении отображается анимация, загруженная из нашего REST API
(с сервера).


Хитрости:

  • AAE поддерживает большинство типов объектов, включая растровые и векторные изображения;
  • Bodymovin позволяет внедрять в конечный JSON-файл все ресурсы при помощи Base64 и благодаря этому можно избежать загрузки ресурсов отдельно на клиентской стороне;
  • вы можете либо рисовать прямо в векторе в AAE либо просто импортировать векторные изображения формата Adobe Illustrator.

К сожалению, у меня не получилось импортировать в AAE файлы формата SVG (я пытался!).

Узнать больше о хитростях и решении возможных проблем вы можете из этой интересной статьи моего коллеги Радослава Сесивы.


Итак, что нам даёт загрузка анимаций с сервера? Самое очевидное преимущество этого подхода — возможность разделить всех участников процесса обновления анимации. Иными словами, чтобы выпустить новую крутую анимацию, дизайнерам достаточно предоставить серверной команде соответствующий JSON-файл. Чтобы удалить анимацию на клиенте, достаточно просто удалить её с сервера. Легко и быстро.

Ещё очень здорово, что одни и те же функции можно реализовать на всех поддерживаемых платформах (iOS, Android, Web), не внося изменений в клиент-серверный протокол, серверный код и в сами файлы анимаций непосредственно на клиенте.

На этом всё. Спасибо за внимание!


Полезные ссылки


© Habrahabr.ru