company_banner

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



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

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

    Архитектура решения


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



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



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

    Экспорт анимаций


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



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

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



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



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



    Наконец, нажатием кнопки 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.

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



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

    Советы и ограничения


    Хитрости:

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

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

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

    Заключение


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

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

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


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


    • +40
    • 4,9k
    • 2
    Badoo
    351,00
    Big Dating
    Поделиться публикацией

    Комментарии 2

      +2
      Очень даже прикольно!
      И отличный путь создания анимации
        +3
        Очень круто, все бы выносили такие вещи по умному на сервер (тему приложения, часть контента, анимации) — жить стало бы гораздо проще. Только это не совсем MVVM, понимаю что код сокращенный но что-то не вижу смысла 3 раза копировать animationModel, если логика запуска анимации все равно ложится на контроллер, т.е. от MVC бы особо не ушли.

        Но это мелочи, подход классный, за полный цикл экспорта отдельный респект, уверен людям пригодится!

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое