Хорошие заметки повсюду

Маркетинговое изображение Goodnotes, на котором изображена женщина, использующая продукт на iPad.

Последние два года команда инженеров Goodnotes работала над проектом по переносу успешного приложения для заметок iPad на другие платформы. В этом тематическом исследовании показано, как приложение года для iPad 2022 года попало в Интернет, ChromeOS, Android и Windows на основе веб-технологий и WebAssembly, повторно использующего тот же код Swift, над которым команда работала более десяти лет.

Логотип Гудноутс.

Почему Goodnotes появился в Интернете, на Android и Windows

В 2021 году Goodnotes был доступен только как приложение для iOS и iPad. Команда инженеров Goodnotes приняла на себя огромную техническую задачу: создать новую версию Goodnotes , но для дополнительных операционных систем и платформ. Продукт должен быть полностью совместим и отображать те же примечания, что и приложение iOS. Любая заметка, сделанная поверх PDF-файла, или любое прикрепленное изображение должны быть эквивалентными и иметь те же штрихи, что и приложение iOS. Любой добавленный штрих должен быть эквивалентен тому, который могут создать пользователи iOS, независимо от используемого пользователем инструмента, например ручки, маркера, перьевой ручки, фигур или ластика.

Предварительный просмотр приложения Goodnotes с рукописными заметками и эскизами.

Основываясь на требованиях и опыте команды разработчиков, команда быстро пришла к выводу, что повторное использование кодовой базы Swift будет лучшим вариантом действий, учитывая, что она уже написана и хорошо протестирована в течение многих лет. Но почему бы просто не перенести уже существующее приложение iOS/iPad на другую платформу или технологию, например Flutter или Compose Multiplatform? Переход на новую платформу потребует переписывания Goodnotes. Это может привести к началу гонки разработки между уже реализованным приложением iOS и новым приложением, которое предстоит создать с нуля, или повлечет за собой остановку новой разработки существующего приложения, пока новая кодовая база не догонит его. Если Goodnotes сможет повторно использовать код Swift, команда сможет извлечь выгоду из новых функций, реализованных командой iOS, в то время как кроссплатформенная команда работает над основами приложения и достигнет паритета функций.

Продукт уже решил ряд интересных задач iOS, добавив такие функции, как:

  • Рендеринг заметок.
  • Синхронизация документов и заметок.
  • Разрешение конфликтов для заметок с использованием бесконфликтных реплицируемых типов данных .
  • Анализ данных для оценки модели ИИ.
  • Поиск по контенту и индексирование документов.
  • Пользовательский опыт прокрутки и анимации.
  • Просмотрите реализацию модели для всех уровней пользовательского интерфейса.

Все их было бы гораздо проще реализовать для других платформ, если бы команда инженеров могла получить кодовую базу iOS, уже работающую для приложений iOS и iPad, и выполнить ее как часть проекта, который Goodnotes мог бы поставлять в виде приложений для Windows, Android или веб-приложений.

Технологический стек Goodnotes

К счастью, существовал способ повторно использовать существующий код Swift в Интернете — WebAssembly (Wasm). Goodnotes создал прототип с использованием Wasm в рамках проекта SwiftWasm с открытым исходным кодом, поддерживаемого сообществом. С помощью SwiftWasm команда Goodnotes смогла сгенерировать двоичный файл Wasm, используя весь уже реализованный код Swift. Этот двоичный файл можно включить в веб-страницу, поставляемую как прогрессивное веб-приложение для Android, Windows, ChromeOS и любой другой операционной системы.

Последовательность развертывания Goodnotes начинается с Chrome, затем Windows, затем Android и в конце других платформ, таких как Linux, и все они основаны на PWA.

Целью было выпустить Goodnotes как PWA и иметь возможность разместить его в магазине каждой платформы. Помимо Swift, языка программирования, уже используемого для iOS, и WebAssembly, используемого для выполнения кода Swift в сети, в проекте использовались следующие технологии:

  • TypeScript: наиболее часто используемый язык программирования для веб-технологий.
  • React и webpack: самый популярный фреймворк и сборщик для Интернета.
  • PWA и сервисные работники: огромные возможности для этого проекта, потому что команда может выпустить наше приложение как автономное приложение, которое работает как любое другое приложение для iOS, и вы можете установить его из магазина или самого браузера.
  • PWABuilder: основной проект, который Goodnotes использует для упаковки PWA в собственный двоичный файл Windows, чтобы команда могла распространять наше приложение из Microsoft Store.
  • Доверенные веб-активности: самая важная технология Android, которую компания использует для распространения PWA как собственного приложения.

Технический стек Goodnotes, состоящий из Swift, Wasm, React и PWA.

На следующем рисунке показано, что реализовано с использованием классического TypeScript и React, а также что реализовано с помощью SwiftWasm и стандартного JavaScript, Swift и WebAssembly. В этой части проекта используется JSKit , библиотека совместимости JavaScript для Swift и WebAssembly, которую команда использует для обработки DOM на экране нашего редактора из нашего кода Swift, когда это необходимо, или даже для использования некоторых API-интерфейсов, специфичных для браузера.

Снимки экрана приложения на мобильных устройствах и настольных компьютерах, показывающие конкретные области рисования, управляемые Wasm, и области пользовательского интерфейса, управляемые React.

Зачем использовать Wasm и Интернет?

Несмотря на то, что Wasm официально не поддерживается Apple, команда инженеров Goodnotes сочла этот подход лучшим решением по следующим причинам:

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

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

Итеративная разработка продукта

Команда применила итеративный подход, чтобы как можно быстрее донести до пользователей что-то. Goodnotes начинался с версии продукта, доступной только для чтения, где пользователи могли получить любой общий документ и прочитать его с любой платформы. Просто имея ссылку, они смогут получить доступ и прочитать те же заметки, которые они написали на своем iPad. На следующем этапе были добавлены функции редактирования, чтобы сделать кроссплатформенные версии эквивалентными iOS.

Два снимка экрана приложения, символизирующие переход от режима «только для чтения» к полнофункциональному продукту.

На разработку первой версии продукта, предназначенного только для чтения, ушло шесть месяцев, следующие девять месяцев были посвящены первому набору функций редактирования и экрану пользовательского интерфейса, на котором вы можете проверить все документы, созданные вами или кем-то, которыми с вами поделились. Кроме того, новые возможности платформы iOS удалось легко портировать в кроссплатформенный проект благодаря SwiftWasm Toolchain. Например, был создан новый тип пера, который легко реализовать кроссплатформенно путем повторного использования тысяч строк кода.

Создание этого проекта было невероятным опытом, и Goodnotes многому научился из него. Вот почему следующие разделы будут посвящены интересным техническим моментам веб-разработки и использованию WebAssembly и таких языков, как Swift.

Начальные препятствия

Работа над этим проектом была очень сложной с разных точек зрения. Первое препятствие, которое обнаружила команда, было связано с набором инструментов SwiftWasm. Инструментальная цепочка стала огромным подспорьем для команды, но не весь код iOS был совместим с Wasm. Например, код, связанный с вводом-выводом или пользовательским интерфейсом, например, реализация представлений, клиентов API или доступ к базе данных, не допускал повторного использования, поэтому команде нужно было начать рефакторинг определенных частей приложения, чтобы иметь возможность повторно использовать их из разных источников. платформенное решение. Большинство PR, созданных командой, представляли собой рефакторинг для абстрактных зависимостей, чтобы команда могла позже заменить их, используя внедрение зависимостей или другие подобные стратегии. Код iOS изначально смешивал сырую бизнес-логику, которую можно было реализовать в Wasm, с кодом, отвечающим за ввод/вывод, и пользовательским интерфейсом, который нельзя было реализовать в Wasm, поскольку Wasm ни того, ни другого не поддерживает. Поэтому код ввода-вывода и пользовательского интерфейса необходимо было переопределить в TypeScript, как только бизнес-логика Swift была готова к повторному использованию между платформами.

Проблемы с производительностью решены

Когда Goodnotes начала работу над редактором, команда выявила некоторые проблемы с процессом редактирования, и в нашу дорожную карту были включены сложные технологические ограничения. Первая проблема была связана с производительностью. JavaScript — это однопоточный язык. Это означает, что у него один стек вызовов и одна куча памяти. Он выполняет код по порядку и должен завершить выполнение фрагмента кода, прежде чем перейти к следующему. Это синхронно, но иногда это может быть вредно. Например, если функция требует некоторого времени для выполнения или должна чего-то ожидать, она на это время все замораживает. И это именно то, что пришлось решить инженерам. Оценка некоторых конкретных путей в нашей кодовой базе, связанных со слоем рендеринга или другими сложными алгоритмами, была проблемой для команды, поскольку эти алгоритмы были синхронными, и их выполнение блокировало основной поток. Команда Goodnotes переписала их, чтобы сделать их быстрее, и провела рефакторинг некоторых из них, сделав их асинхронными. Они также представили стратегию доходности, чтобы приложение могло остановить выполнение алгоритма и продолжить его позже, позволяя браузеру обновлять пользовательский интерфейс и избегать пропуска кадров. Это не было проблемой для приложения iOS, поскольку оно может использовать потоки и оценивать эти алгоритмы в фоновом режиме, в то время как основной поток iOS обновляет пользовательский интерфейс.

Еще одним решением, которое пришлось решить команде инженеров, был перенос пользовательского интерфейса, основанного на HTML-элементах, прикрепленных к DOM, в пользовательский интерфейс документа, основанный на полноэкранном холсте. Проект начал отображать все заметки и контент, связанный с документом, как часть структуры DOM с использованием элементов HTML, как и любая другая веб-страница, но в какой-то момент перешел на полноэкранный холст, чтобы улучшить производительность на устройствах начального уровня за счет сокращение времени работы браузера над обновлениями DOM.

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

  • Больше разгружайте основной поток, часто используя веб-воркеры для тяжелых алгоритмов.
  • С самого начала используйте экспортированные и импортированные функции вместо библиотеки взаимодействия JS-Swift, чтобы они могли снизить влияние на производительность при выходе из контекста Wasm. Эта библиотека взаимодействия JavaScript полезна для получения доступа к DOM или браузеру, но она медленнее, чем собственные экспортированные функции Wasm.
  • Убедитесь, что код позволяет использовать OffscreenCanvas скрыто, чтобы приложение могло разгрузить основной поток и передать все использование Canvas API веб-работнику, максимально повышая производительность приложений при написании заметок.
  • Перенесите все выполнение, связанное с Wasm, на веб-воркера или даже в пул веб-воркеров, чтобы приложение могло снизить рабочую нагрузку основного потока.

Текстовый редактор

Еще одна интересная проблема была связана с одним конкретным инструментом — текстовым редактором. Реализация этого инструмента для iOS основана на NSAttributedString , небольшом наборе инструментов, использующем RTF . Однако эта реализация несовместима со SwiftWasm, поэтому кросс-платформенная команда была вынуждена сначала создать собственный синтаксический анализатор на основе грамматики RTF , а затем реализовать возможность редактирования путем преобразования RTF в HTML и наоборот. Тем временем команда iOS начала работать над новой реализацией этого инструмента, заменяя использование RTF собственной моделью, чтобы приложение могло представлять стилизованный текст в удобной форме для всех платформ, использующих один и тот же код Swift.

Текстовый редактор Goodnotes.

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

Итеративные релизы

Эволюция проекта за последние два года была невероятной. Команда начала работу над версией проекта, доступной только для чтения, и несколько месяцев спустя выпустила совершенно новую версию с множеством возможностей редактирования. Чтобы часто выпускать изменения кода в производство, команда решила широко использовать флаги функций. Для каждого выпуска команда могла включать новые функции, а также выпускать изменения кода, реализующие новые функции, которые пользователь увидит несколько недель спустя. Однако есть кое-что, что, по мнению команды, они могли бы улучшить! Они считают, что введение динамической системы флагов функций помогло бы ускорить процесс, поскольку устранило бы необходимость повторного развертывания для изменения значений флагов. Это даст Goodnotes больше гибкости, а также ускорит развертывание новой функции, поскольку Goodnotes не нужно будет связывать развертывание проекта с выпуском продукта.

Оффлайн работа

Одна из основных функций, над которой работала команда, — это офлайн-поддержка. Возможность редактировать ваши документы и изменять их — это одна из функций, которую вы ожидаете от любого подобного приложения. Однако это непростая функция, поскольку Goodnotes поддерживает совместную работу. Это означает, что все изменения, внесенные разными пользователями на разных устройствах, должны появиться на каждом устройстве, не требуя от пользователей разрешения каких-либо конфликтов. Goodnotes давно решил эту проблему, используя CRDT под капотом. Благодаря этим бесконфликтным реплицируемым типам данных Goodnotes может объединять все изменения, внесенные в любой документ любым пользователем, и объединять изменения без каких-либо конфликтов слияния. Использование IndexedDB и хранилища, доступного для веб-браузеров, стало огромным стимулом для совместной работы в автономном режиме в Интернете.

Приложение Goodnotes работает в автономном режиме.

Кроме того, открытие веб-приложения Goodnotes приводит к первоначальной стоимости загрузки около 40 МБ из-за размера двоичного файла Wasm. Первоначально команда Goodnotes полагалась исключительно на обычный кеш браузера для самого пакета приложений и большинства конечных точек API, которые они используют, но, оглядываясь назад, раньше могла бы получить выгоду от более надежного Cache API и сервис-воркеров. Первоначально команда уклонялась от этой задачи из-за ее предполагаемой сложности, но в конце концов поняла, что Workbox сделал ее намного менее пугающей.

Рекомендации при использовании Swift в Интернете

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

  • Проверьте, какой код вы хотите использовать повторно. Если бизнес-логика вашего приложения реализована на стороне сервера, вероятно, вы захотите повторно использовать код пользовательского интерфейса, и Wasm вам здесь не поможет. Команда кратко рассмотрела Tokamak , SwiftUI-совместимую среду для создания браузерных приложений с помощью WebAssembly, но она не была достаточно зрелой для нужд приложения. Однако, если ваше приложение имеет надежную бизнес-логику или алгоритмы, реализованные как часть клиентского кода, Wasm станет вашим лучшим другом.
  • Убедитесь, что ваша кодовая база Swift готова. Шаблоны проектирования программного обеспечения для уровня пользовательского интерфейса или конкретные архитектуры, создающие четкое разделение между логикой пользовательского интерфейса и бизнес-логикой, будут очень полезны, поскольку вы не сможете повторно использовать реализацию уровня пользовательского интерфейса. Принципы чистой архитектуры или шестиугольной архитектуры также будут иметь основополагающее значение, поскольку вам придется внедрять и предоставлять зависимости для всего кода, связанного с вводом-выводом, и это будет намного проще сделать, если вы будете следовать этим архитектурам, где детали реализации определяются как абстракции и широко используется принцип инверсии зависимостей .
  • Wasm не предоставляет код пользовательского интерфейса. Поэтому определитесь с инфраструктурой пользовательского интерфейса, которую вы хотите использовать в Интернете.
  • JSKit поможет вам интегрировать ваш код Swift с JavaScript, но имейте в виду, что если у вас есть горячий путь, пересечение моста JS-Swift может оказаться дорогостоящим, и вам придется заменить его экспортированными функциями. Вы можете узнать больше о том, как работает JSKit, в официальной документации и о динамическом поиске членов в Swift, скрытой жемчужине! почта.
  • Возможность повторного использования вашей архитектуры будет зависеть от архитектуры, которой следует ваше приложение, и от используемой вами библиотеки механизма выполнения асинхронного кода. Такие шаблоны, как MVVP или составная архитектура, помогут вам повторно использовать модели представлений и часть логики пользовательского интерфейса, не связывая реализацию с зависимостями UIKit , которые вы не можете использовать с Wasm. RXSwift и другие библиотеки могут быть несовместимы с Wasm, поэтому имейте это в виду, поскольку вам придется использовать OpenCombine , async/await и потоки в коде Swift Goodnotes.
  • Сожмите двоичный файл Wasm с помощью gzip или brotli. Имейте в виду, что размер двоичного файла будет довольно большим для классических веб-приложений.
  • Даже если вы можете использовать Wasm без PWA, убедитесь, что вы включили хотя бы сервисного работника, даже если ваше веб-приложение не имеет манифеста или вы не хотите, чтобы пользователь его устанавливал. Сервис-воркер бесплатно сохранит и обслужит двоичный файл Wasm и все ресурсы приложения, поэтому пользователю не придется загружать их каждый раз, когда он открывает ваш проект.
  • Имейте в виду, что нанять сотрудников может оказаться сложнее, чем ожидалось. Возможно, вам придется нанять сильных веб-разработчиков с некоторым опытом работы на Swift или сильных разработчиков Swift с некоторым опытом работы в Интернете. Если вы сможете найти инженеров широкого профиля с некоторыми знаниями по обеим платформам, это будет здорово.

Выводы

Создание веб-проекта с использованием сложного технологического стека и одновременной работы над продуктом, полным задач, — это невероятный опыт. Это будет сложно, но оно того стоит. Goodnotes никогда бы не выпустила версию для Windows, Android, ChromeOS и Интернета, одновременно работая над новыми функциями приложения iOS, не используя этот подход. Благодаря этому технологическому стеку и команде инженеров Goodnotes, Goodnotes теперь повсюду, и команда готова продолжить работу над следующими задачами! Если вы хотите узнать больше об этом проекте, вы можете посмотреть выступление команды Goodnotes на NSSpain 2023 . Обязательно попробуйте Goodnotes для Интернета !