Нативный backend-driven UI в iOS приложении на базе Editor.js

Как быстро интегрировать и нативно отрисовать контент статейного формата у себя в мобильном приложении, почти никак не нагрузив проект и всего с помощью нескольких строк кода? В этом материале я опишу наш способ решения такой задачи.

Материал может быть интересен любым мобильным продуктовым командам, где есть потребность в backend-driven верстке, а контент приложения - статьи, соглашения, документы, типовые карточки, списки и т.д.

Про инструмент

Инструмент, о котором пойдет речь - созданный нашей командой фреймворк Editor.js Kit. Он превращает JSON данные формата Editor.js в нативную мобильную верстку 👇

Нативный backend-driven UI в iOS приложении на базе Editor.js

Фреймворк выложен в open-source и доступен на github для iOS (Swift) и Android (Kotlin). В рамках этой статьи с практической стороны раскрывается только iOS часть.

Предыстория

Пару лет назад наша команда столкнулась с задачей отрисовки в мобильном приложении новостных статей, пользовательских соглашений, экранов с описанием товара и других отформатированных массивов текста, чередующихся картинками, форматированными ссылками и списками. В общем, экранов статейного формата, как на TJ / VC. Контент должен был раздаваться бэкендом, а уже конкретный клиент (web / iOS / Android) форматирует и рендерит его в соответствии с представлениями о хорошем UX платформы и чувством прекрасного.

Задача, с одной стороны, тривиальная. Но, во-первых, экраны с таким контентом встречаются повсеместно почти во всех приложениях, и было бы хорошо придумать переиспользуемое универсальное решение. А во-вторых, вариант взять UITextView / WKWebView и скормить ему html верстку нам не подходит, поскольку данные должны быть легковесными, а верстка - нативной.

Было принято решение: пилим фреймворк.

Формат данных: Editor.js

При выборе формата для создания, хранения и передачи контента учитывали три фактора:

  • Команда редакторов должна создавать статьи с форматированием в каком-то юзер-френдли веб-редакторе;
  • Бэкенд API должен уметь принимать написанную статью с сохранением форматирования, хранить ее и раздавать ее всем клиентам на любые платформы в едином формате;
  • Мобильный клиент должен принимать контент статьи, парсить его и рендерить нативно.

Думали не долго: наткнулись на Editor.js - тот самый редактор, про который на VC уже была статья. По словам авторов из команды CodeX, именно этот инструмент используется для написания статей на vc.ru, TJ и DTF.

Основная концепция — блочная структура и чистые данные в виде JSON на выходе. В отличие от большинства редакторов, где пользователь работает с текстом внутри одной редактируемой обертки, в Editor.js каждый структурный элемент статьи — блок — это отдельный редактируемый элемент. Блоки могут быть какие угодно: абзацы, заголовки, цитаты, списки, изображения, твиты, опросы и так далее.

Поддерживаемые блоки

Из коробки фреймворк поддерживает семь типов блоков: заголовок, параграф, изображение с подписью, ссылка, список нумерованный / буллет лист, разделитель и чистый html.

Нативный backend-driven UI в iOS приложении на базе Editor.js

Парсинг контента

JSON данные могут отдаваться как с backend API, так и лежать локально в бандле или памяти смартфона - подготовка к его использованию будет одинакова и максимально проста в любом случае. Если кратко:

/// JSON данные `data: Data` превращаются в список блоков, готовых к отрисовке let myBlocks = try EJKit.shared.decode(data: data)

Подключение на экран

Для отрисовки экранов со скроллом мы почти всегда используем UICollectionView вместо UITableView в силу большей гибкости лейаута. Поэтому поддержку коллекции сделали в первую очередь. Любителей таблиц успокою - их поддержка легко добавляется адаптером по образцу коллекции.

Есть два основных сценария интеграции верстки себе на экран. Первый - когда ваш экран состоит только из Editor.js блоков. Это максимально просто делается через адаптер:

import EditorJSKit /// Не забываем конформить `EJCollectionDataSource` class ViewController: UIViewController, EJCollectionDataSource { /// Инстанс кита let kit = EJKit.shared lazy var collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: UICollectionViewFlowLayout()) lazy var adapter = EJCollectionViewAdapter(collectionView: collectionView) /// Список блоков, который мы парсим var blocks: EJBlocksList? /// реализация протокола EJCollectionDataSource var data: EJBlocksList? { blocks } /** */ override func viewDidLoad() { super.viewDidLoad() adapter.dataSource = self let data: Data // ваши json данные в формате Editor.js blocks = try EJKit.shared.decode(data: data) collectionView.reloadData() } }

Второй сценарий - в случае, если вам надо встроить блоки в уже существующий экран с коллекцией, в любое его место. Для этого удобно использовать EJCollectionRenderer, который напрямую реализует методы dataSource и delegate коллекции и позволяет добавлять блоки в коллекцию в любом удобном порядке. Пример такой интеграции - в Readme на github.

Кастомизация стилей

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

Рассмотрим базовую кастомизацию на примере заголовков:

{ "type": "header", "data": { "text": "Header 1", "level": 1 } }

Допустим, мы хотим изменить размер шрифта для каждого уровня заголовка, выровнять их по центру и значительно увеличить отступы сверху и снизу. За стиль заголовка отвечает протокол EJHeaderBlockStyle, который нужно реализовать:

struct CustomHeaderStyle: EJHeaderBlockStyle { let alignment: NSTextAlignment = .center /** */ func font(forHeaderLevel level: Int) -> UIFont { switch level { case 1: return UIFont.systemFont(ofSize: 30, weight: .bold) case 2: return UIFont.systemFont(ofSize: 24, weight: .bold) default: return UIFont.systemFont(ofSize: 20, weight: .semibold) } } /** */ func topInset(forHeaderLevel level: Int) -> CGFloat { return level == 1 ? 10 : 6 } /** */ func bottomInset(forHeaderLevel level: Int) -> CGFloat { return level == 1 ? 20 : 12 } }

Подключаем стиль:

EJKit.shared.set(style: CustomHeaderStyle(), for: EJNativeBlockType.header)

В результате получаем задуманное форматирование без каких-либо изменений в коде вью или в JSON данных:

Нативный backend-driven UI в iOS приложении на базе Editor.js

Кастомные блоки

Встроить в полотно скролла можно абсолютно любой блок, и для этого не понадобится писать слишком много кода.

Каждый JSON блок представляет собой два верхнеуровневых поля: type (тип блока) и data (JSON контент, который зависит от типа блока). Для примера придумаем и сделаем выноску - callout:

{ "type" : "callout", "data" : { "emoji": "🙀", "text" : "This is a custom block which was easily integrated with a few lines of code." } }

Первым делом создадим кастомный тип-перечисление для наших собственных блоков, сразу добавив туда callout:

enum CustomBlockType: String, EJAbstractBlockType { case callout }

Затем пропишем модель для поля data, важно проставить протоколы:

public struct CalloutBlockContentItem: EJAbstractBlockContentItem { let text: String let emoji: String }

Следующим шагом пропишем стиль для нашей выноски, также реализуя EJBlockStyle:

struct CalloutBlockStyle: EJBlockStyle { // желтенький let backgroundColor = UIColor(red: 1, green: 0.78, blue: 0, alpha: 0.2) let cornerRadius: CGFloat = 6 let lineSpacing: CGFloat = 10 let font = UIFont.systemFont(ofSize: 16) let textColor = UIColor.black let emojiFont = UIFont.systemFont(ofSize: 24) }

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

final class CalloutBlockView: UIView, EJBlockView { private let emoji = UILabel() private let label = UILabel() /** Расставляем тексты в лейблы и применяем стиль */ func configure(withItem item: CalloutBlockContentItem, style: EJBlockStyle?) { emoji.text = item.emoji label.text = item.text guard let style = style as? CalloutBlockStyle else { return } backgroundColor = style.backgroundColor layer.cornerRadius = style.cornerRadius emoji.font = style.emojiFont label.font = style.font label.textColor = style.textColor } /** Здесь нужно посчитать размер для блока с учетом его стиля */ static func estimatedSize(for item: CalloutBlockContentItem, style: EJBlockStyle?, boundingWidth: CGFloat) -> CGSize { return CGSize(width: boundingWidth, height: 100) } }

Последний шаг: подключаем созданный блок к EJKit.

// Связываем элементы блока в единый каркас let calloutBlock = EJCustomBlock( type: CustomBlockType.callout, contentClass: BlockContent.Single<CalloutBlockContentItem>.self, viewClass: CalloutBlockView.self) // Регистрируем блок EJKit.shared.register(customBlock: calloutBlock) // Регистрируем стиль блока EJKit.shared.style.set(style: CalloutBlockStyle(), for: CustomBlockType.callout)

Готово! Смотрим на результат в виде светло-желтой выноски с шокированным котом в центре экрана:

Нативный backend-driven UI в iOS приложении на базе Editor.js

Недостатки сегодня

  • Поддерживается только отображение, контента, возможности редактирования блоков прямо с клавиатуры на смартфоне нет.
  • Интерактив с контентом блоков отсутствует (кроме нажатия на гиперссылки и форматированные ссылки). Конечно, можно написать свой кастомный блок и прописать там всё, что душе угодно. Но никакой архитектуры на уровне фреймворка под поддержку интерактива не предусмотрено.
  • Стили для семи поддерживаемых из коробки типов блоков не всеобъемлющи. У текста помимо цвета, шрифта и отступов есть, как минимум, междустрочный интервал, кернинг и т.п. Но опять-таки, ничто не мешает переопределить тот или иной блок или стиль под дизайн-систему вашего проекта.

Выводы

Editor.js Kit хорошо подойдет тем, у кого в мобильном продукте хотя бы на нескольких экранах нужно реализовать backend-driven верстку. При довольно малых временных затратах можно интегрировать к себе в приложение эстетичный нативный юзер интерфейс в соответствии с вашим дизайн-кодом, не захламив при этом проект сторонними зависимостями.

Наравне с no-code и low-code продуктами, наше решение призвано сократить время, которое команда тратит на дизайн тривиальных задач, а программист на написание boilerplate кода, что особенно важно для стартапов и командам на этапах создания MVP продукта.

Всем интересующимся - добро пожаловать к нам на github: iOS и Android.

Над фреймворком работала команда Upstarts.work

Авторы и контрибьюторы: Вадим, Руслан, Иван, Исрапил, Александр

Авторы редактора Editor.js – CodeX Team

38
7 комментариев

Комментарий недоступен

3
Ответить

1. На производительность может влиять только реализация верстки в отдельных блоках. В тех, что поддерживаются из коробки - нет ничего тяжелого, автолейаут обычный, пара лейблов и картинка - максимум. Если реализовать кастомный сложный блок (например, положить в него... игру), и поместить его в скролл, то не исключено, что скролл будет подлагивать - но это уже другая история :)
2. Не совсем, но такая цель тоже легко реализуема. На бэкенд можно прокидывать версию приложения и в зависимости от нее возвращать контент. Основная наша цель была - уменьшить количество будущих трудозатрат для реализации такой тривиальной задачи.
3. Первую MVP версию собрали на фуллтайме за 2 недели, потом еще 4 месяца понемногу улучшали и расширяли. После чего был перерыв, и вот сейчас вернулись к нему, значительно обновили кодовую базу для iOS, и для Android тоже на очереди.
4. Мы с помощью фреймворка решали задачи вроде реализации маркетплейсов с динамическими страницами товаров, описаниями, новостными статьями, обзорами. Тут точно оправдано, время было сэкономлено не только на фронте, но и на бэке - там большинство контента хранили как jsonb в базе, и не заморачивались со специальными таблицами или orm.

1
Ответить

Спасибо за развитие и поддержку продукта!

2
Ответить

Спасибо. Взял на заметку.

1
Ответить

Комментарий недоступен

Ответить

Поддерживается только отображение, контента, возможности редактирования блоков прямо с клавиатуры на смартфоне нет.

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

Ну а правила, которые придуманы не нами - так это не баг, а фича. Зачем придумывать что-то новое, если есть отличный и активно используемый Editor.js с открытым исходным кодом. Мы на основе готового формата сделали инструмент, который значительно упрощает процесс верстки.

Ответить