Полгода назад мы представили одну из самых впечатляющих функций 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), не внося изменений в клиент-серверный протокол, серверный код и в сами файлы анимаций непосредственно на клиенте.
На этом всё. Спасибо за внимание!
Полезные ссылки
- "Behind the scenes with importing Adobe After Effects animation into Badoo iOS app" by Radoslaw Cieciwa;
- Bodymovin plugin for AAE by AirbnbEng;
- Проект целиком доступен здесь на GitHub;
- Installing bodymovin plugin for AAE by AirbnbEng;
- Lottie iOS library by AirbnbEng;
- "REST API with Swift on Vapor" Валерий Чевтаев
ZXP plugin installer for AAE.
Автор: Valeron