Buenas notas en todas partes

Imagen de marketing de Goodnotes que muestra a una mujer usando el producto en un iPad.

Durante los últimos dos años, el equipo de ingeniería de Goodnotes ha estado trabajando en un proyecto para llevar la exitosa app de toma de notas para iPad a otras plataformas. En este caso de éxito, se muestra cómo la app del año 2022 para iPad llegó a la Web, ChromeOS, Android y Windows con la tecnología de tecnologías web y WebAssembly reutilizando el mismo código Swift en el que ha estado trabajando el equipo por más de diez años.

Logotipo de Goodnotes.

Por qué Goodnotes llegó a la Web, Android y Windows

En 2021, Goodnotes solo estaba disponible como app para iOS y iPad. El equipo de ingeniería de Goodnotes aceptó un gran desafío técnico: crear una versión nueva de Goodnotes, pero para plataformas y sistemas operativos adicionales. El producto debe ser totalmente compatible y renderizar las mismas notas que la aplicación para iOS. Cualquier nota que se tome sobre un PDF o cualquier imagen adjunta debe ser equivalente y mostrar los mismos trazos que muestra la app para iOS. Cualquier trazo agregado debe ser equivalente al que los usuarios de iOS pueden crear, independientemente de la herramienta que esté usando, por ejemplo, bolígrafo, resaltador, pluma fuente, formas o borrador.

Vista previa de la app de Goodnotes con bocetos y notas escritas a mano.

Según los requisitos y la experiencia del equipo de ingeniería, el equipo concluyó rápidamente que reutilizar la base de código Swift sería el mejor curso de acción, dado que ya se había escrito y probado durante muchos años. Pero ¿por qué no trasladar la aplicación existente para iOS o iPad a otra plataforma o tecnología como Flutter o Compose multiplataforma? Para cambiarse a una plataforma nueva, implicaría una reescritura de Goodnotes. Si lo haces, es posible que se inicie una carrera de desarrollo entre la aplicación para iOS ya implementada y una que se compilará a partir de una aplicación nueva, o que se detenga el desarrollo nuevo en la aplicación existente mientras la nueva base de código está al día. Si Goodnotes pudiese reutilizar el código Swift, el equipo podría beneficiarse de las nuevas funciones que implementa el equipo de iOS mientras el equipo multiplataforma trabajaba en los aspectos básicos de la app y alcanzaría la paridad de funciones.

El producto ya había resuelto varios desafíos interesantes en iOS para agregar funciones como las siguientes:

  • Renderización de notas.
  • Sincronización de documentos y notas.
  • Resolución de conflictos para notas mediante los tipos de datos replicados sin conflictos
  • Análisis de datos para la evaluación de modelos de IA.
  • Búsqueda de contenido e indexación de documentos
  • Animaciones y experiencia de desplazamiento personalizadas
  • Consulta la implementación del modelo para todas las capas de la IU.

Todos serían mucho más fáciles de implementar en otras plataformas si el equipo de ingeniería pudiera hacer que la base de código de iOS ya funcione para las aplicaciones de iOS y iPad y ejecutarla como parte de un proyecto que Goodnotes podría ofrecer como aplicaciones para Windows, Android o la Web.

Pila tecnológica de Goodnotes

Por suerte, hubo una forma de reutilizar el código Swift existente en la Web: WebAssembly (Wasm). Goodnotes compiló un prototipo usando Wasm con el proyecto de código abierto SwiftWasm y que mantiene la comunidad. Con SwiftWasm, el equipo de Goodnotes podría generar un objeto binario de Wasm con todo el código Swift ya implementado. Este objeto binario podría incluirse en una página web que se envía como una aplicación web progresiva para Android, Windows, ChromeOS y todos los demás sistemas operativos.

Secuencia de lanzamiento de Goodnotes que comienza con Chrome, luego Windows, luego Android y otras plataformas como Linux al final, todas basadas en la AWP.

El objetivo era lanzar Goodnotes como una AWP y poder incluirla en la tienda de cada plataforma. Además de Swift, el lenguaje de programación que ya se usa en iOS y WebAssembly que se usa para ejecutar código Swift en la Web, el proyecto utilizó las siguientes tecnologías:

  • TypeScript: Es el lenguaje de programación más usado para tecnologías web.
  • React y webpack: El framework y agrupador más popular para la Web.
  • AWP y service workers: Son importantes habilitadores para este proyecto, ya que el equipo podría enviar nuestra app como una aplicación sin conexión que funciona como cualquier otra app para iOS y tú puedes instalarla desde la tienda o desde el propio navegador.
  • PWABuilder: Es el proyecto principal que usa Goodnotes para unir la AWP en un objeto binario nativo de Windows, de modo que el equipo pueda distribuir nuestra app desde Microsoft Store.
  • Actividades web de confianza: La tecnología de Android más importante que usa la empresa para distribuir nuestra AWP como una aplicación nativa de forma interna.

Pila tecnológica de Goodnotes que consta de Swift, Wasm, React y AWP

En la siguiente figura, se muestra lo que se implementa con la versión clásica de TypeScript y React, y lo que se implementa con SwiftWasm y JavaScript vanilla, Swift y WebAssembly. En esta parte del proyecto, se usa JSKit, una biblioteca de interoperabilidad de JavaScript para Swift y WebAssembly que el equipo usa a fin de controlar el DOM en la pantalla de nuestro editor desde nuestro código Swift cuando sea necesario o incluso usar algunas APIs específicas del navegador.

Capturas de pantalla de la app en dispositivos móviles y computadoras de escritorio que muestran las áreas de dibujo específicas que impulsa Wasm y las áreas de la IU que controla React.

¿Por qué usar Wasm y la web?

Aunque Wasm no tiene compatibilidad oficial con Apple, las siguientes razones son las razones por las que el equipo de ingeniería de Goodnotes sintió que este enfoque fue la mejor decisión:

  • La reutilización de más de 100,000 líneas de código
  • La capacidad de continuar el desarrollo en el producto principal y, al mismo tiempo, contribuir a las apps multiplataforma
  • El poder de acceder a todas las plataformas lo antes posible mediante un proceso de desarrollo iterativo
  • Tener control para renderizar el mismo documento sin duplicar toda la lógica empresarial, además de introducir diferencias en nuestras implementaciones
  • Beneficiarse de todas las mejoras de rendimiento que se realizan en todas las plataformas al mismo tiempo (y todas las correcciones de errores implementadas en cada una de ellas)

La reutilización de más de 100, 000 líneas de código y la lógica empresarial que implementó nuestra canalización de renderización fue fundamental. Al mismo tiempo, hacer que el código Swift sea compatible con otras cadenas de herramientas les permite reutilizar este código en diferentes plataformas en el futuro si es necesario.

Desarrollo iterativo de productos

El equipo adoptó un enfoque iterativo para ofrecer algo a los usuarios lo más rápido posible. Goodnotes comenzó con una versión de solo lectura del producto en la que los usuarios podían obtener cualquier documento compartido y leerlo desde cualquier plataforma. Con solo un vínculo, podrían acceder y leer las mismas notas que escribieron desde su iPad. La siguiente fase se agregó a las funciones de edición para que las versiones multiplataforma sean equivalentes a la de iOS.

Dos capturas de pantalla de la app que simbolizan el paso de solo lectura al producto con todas las funciones.

La primera versión del producto de solo lectura tardó seis meses en desarrollarse, y los nueve meses siguientes estuvieron dedicados al primer conjunto de funciones de edición y a la pantalla de la IU, donde puedes verificar todos los documentos que creaste o que alguien compartió contigo. Además, las funciones nuevas de la plataforma de iOS fueron fáciles de transferir al proyecto multiplataforma gracias a la cadena de herramientas de SwiftWasm. A modo de ejemplo, se creó un nuevo tipo de lápiz y se implementó con facilidad multiplataforma mediante la reutilización de miles de líneas de código.

La creación de este proyecto fue una experiencia increíble, y Goodnotes ha aprendido mucho de él. Es por eso que las siguientes secciones se enfocarán en puntos técnicos interesantes sobre el desarrollo web y el uso de WebAssembly y lenguajes como Swift.

Obstáculos iniciales

Trabajar en este proyecto fue muy desafiante desde diferentes puntos de vista. El primer obstáculo que el equipo encontró estaba relacionado con la cadena de herramientas de SwiftWasm. La cadena de herramientas era un gran habilitador para el equipo, pero no todo el código de iOS era compatible con Wasm. Por ejemplo, el código relacionado con E/S o la IU, como la implementación de vistas, los clientes de API o el acceso a la base de datos, no era reutilizable, por lo que el equipo tuvo que comenzar a refactorizar partes específicas de la app para poder reutilizarlas desde la solución multiplataforma. La mayoría de las PR que creó el equipo eran refactorizaciones para abstraer dependencias, de modo que el equipo pudiera reemplazarlas más tarde con la inserción de dependencias o alguna otra estrategia similar. Originalmente, el código de iOS mezclaba una lógica empresarial sin procesar que podía implementarse en Wasm con el código responsable de la entrada y salida y la interfaz de usuario que no se podía implementar en Wasm porque tampoco lo admite. Por lo tanto, se debía reimplementar el código de IU y de IO en TypeScript una vez que la lógica empresarial de Swift estuviera lista para reutilizarse entre plataformas.

Problemas de rendimiento resueltos

Una vez que Goodnotes comenzó a trabajar en el editor, el equipo identificó algunos problemas con la experiencia de edición, y se agregaron restricciones tecnológicas desafiantes a nuestra hoja de ruta. El primer problema estaba relacionado con el rendimiento. JavaScript es un lenguaje de un solo subproceso. Esto significa que tiene una pila de llamadas y un montón de memoria. Ejecuta el código en orden y debe terminar de ejecutar un fragmento antes de pasar al siguiente. Es síncrono, pero a veces puede ser perjudicial. Por ejemplo, si una función tarda un poco en ejecutarse o tiene que esperar algo, todo se inmoviliza mientras tanto. Eso es exactamente lo que los ingenieros tuvieron que resolver. Evaluar algunas rutas específicas en nuestra base de código relacionadas con la capa de renderización o con otros algoritmos complejos representaba un problema para el equipo, ya que estos algoritmos eran síncronos y su ejecución bloqueaba el subproceso principal. El equipo de Goodnotes las reescribió para hacerlas más rápidas y refactorizó algunas de ellas para volverlas asíncronas. También incorporaron una estrategia de rendimiento para que la app pudiera detener la ejecución del algoritmo y continuarla más tarde, lo que le permitió al navegador actualizar la IU y evitar que se descartaran fotogramas. Esto no fue un problema para la aplicación para iOS, ya que puede usar subprocesos y evaluar estos algoritmos en segundo plano mientras el subproceso principal de iOS actualiza la interfaz de usuario.

Otra solución que el equipo de ingeniería tuvo que resolver fue migrar una IU basada en elementos HTML adjuntos al DOM a una IU de documento basada en un lienzo de pantalla completa. El proyecto comenzó a mostrar todas las notas y el contenido relacionados con un documento como parte de la estructura del DOM con elementos HTML como lo haría cualquier otra página web, pero en algún momento migró a un lienzo de pantalla completa para mejorar el rendimiento en dispositivos de gama baja reduciendo el tiempo de trabajo del navegador en las actualizaciones del DOM.

El equipo de ingeniería identificó los siguientes cambios como aspectos que podrían haber reducido algunos de los problemas encontrados si se hubieran realizado al comienzo del proyecto.

El editor de texto

Otro problema interesante estaba relacionado con una herramienta específica, el editor de texto. La implementación de iOS para esta herramienta se basa en NSAttributedString, un conjunto de herramientas pequeño que usa RTF de forma interna. Sin embargo, como esta implementación no es compatible con SwiftWasm, el equipo multiplataforma tuvo que crear primero un analizador personalizado basado en la gramática de RTF y, luego, implementar la experiencia de edición mediante la transformación de RTF en HTML, y viceversa. Mientras tanto, el equipo de iOS comenzó a trabajar en la nueva implementación de esta herramienta, que reemplaza el uso de RTF por un modelo personalizado para que la app represente texto con estilo de una manera sencilla para todas las plataformas que comparten el mismo código Swift.

El editor de texto Goodnotes.

Este desafío fue uno de los puntos más interesantes de la hoja de ruta del proyecto porque se resolvió de manera iterativa en función de las necesidades del usuario. Fue un problema de ingeniería que se resolvió con un enfoque centrado en el usuario, en el que el equipo debía reescribir parte del código para poder renderizar texto, de modo que se habilitó la edición de texto en una segunda versión.

Versiones iterativas

La evolución del proyecto en los últimos dos años ha sido increíble. El equipo comenzó a trabajar en una versión de solo lectura del proyecto y, meses después, envió una versión nueva con muchas funciones de edición. Para lanzar los cambios de código a producción con frecuencia, el equipo decidió usar marcas de funciones de forma exhaustiva. Para cada versión, el equipo podía habilitar funciones nuevas y lanzar cambios en el código que implementen funciones nuevas que el usuario vería semanas después. Sin embargo, el equipo cree que podría haber mejorado. Creen que introducir un sistema de marcas de funciones dinámicas habría ayudado a acelerar el proceso, ya que quitaría la necesidad de volver a implementar para cambiar los valores de las marcas. Esto le daría a Goodnotes más flexibilidad y también aceleraría la implementación de la función nueva, ya que Goodnotes no necesitaría vincular la implementación del proyecto con la versión del producto.

Trabajo sin conexión

Una de las funciones principales en las que trabajó el equipo es el soporte sin conexión. Poder editar tus documentos y modificarlos es una función que esperas de cualquier aplicación como esta. Sin embargo, esta no es una función simple porque Goodnotes admite la colaboración. Esto significa que todos los cambios que realizan los diferentes usuarios en diferentes dispositivos deben terminar en todos los dispositivos sin pedirles a los usuarios que resuelvan ningún conflicto. Goodnotes resolvió este problema hace mucho tiempo con CRDT de forma interna. Gracias a estos tipos de datos replicados sin conflictos, Goodnotes puede combinar todos los cambios realizados en cualquier documento por cualquier usuario y combinarlos sin problemas de fusión. El uso de IndexedDB y el almacenamiento disponible para los navegadores web fue un gran habilitador para la experiencia colaborativa sin conexión en la Web.

La app de Goodnotes funciona sin conexión.

Además, abrir la app web de Goodnotes genera un costo inicial de descarga inicial de alrededor de 40 MB debido al tamaño del objeto binario de Wasm. En un principio, el equipo de Goodnotes dependía exclusivamente de la caché del navegador normal del paquete de aplicación y de la mayoría de los extremos de la API que usa, pero, en retrospectiva, podría haberse beneficiado de la API de Cache y los service workers más confiables. En un principio, el equipo evitó esta tarea debido a la supuesta complejidad, pero, al final, se dio cuenta de que Workbox la hacía mucho menos aterradora.

Recomendaciones para usar Swift en la Web

Si tienes una aplicación para iOS con mucho código que deseas reutilizar, prepárate porque estás a punto de comenzar un viaje increíble. Antes de empezar, hay algunos consejos que pueden resultarte interesantes.

  • Revisa el código que quieres reutilizar. Si la lógica empresarial de tu app se implementa en el servidor, es probable que quieras reutilizar tu código de IU, y Wasm no te ayudará en este caso. El equipo observó brevemente Tokamak, un framework compatible con SwiftUI para compilar apps de navegador con WebAssembly, pero no era lo suficientemente maduro para las necesidades de la app. Sin embargo, si tu app tiene una lógica empresarial sólida o algoritmos implementados como parte del código del cliente, Wasm será tu mejor amigo.
  • Asegúrate de que tu base de código Swift esté lista. Los patrones de diseño de software para la capa de la IU o arquitecturas específicas que creen una separación sólida entre la lógica de la IU y la lógica empresarial serán muy útiles, ya que no podrás reutilizar la implementación de la capa de la IU. La arquitectura limpia o los principios de la arquitectura hexagonal también serán fundamentales, ya que tendrás que inyectar y proporcionar dependencias para todo el código relacionado con E/S, y será mucho más fácil de hacer si sigues estas arquitecturas en las que los detalles de implementación se definen como abstracciones y el principio de inversión de dependencias se usa mucho.
  • Wasm no proporciona un código de IU. Por lo tanto, decide qué framework de IU deseas usar para la Web.
  • JSKit te ayudará a integrar tu código Swift en JavaScript, pero ten en cuenta que, si tienes una ruta de acceso caliente, cruzar el puente JS-Swift puede ser costoso y deberás reemplazarlo por funciones exportadas. Para obtener más información sobre el funcionamiento interno de JSKit, consulta la documentación oficial y la publicación Dynamic Member Lookup in Swift, a hidden gem!.
  • La posibilidad de reutilizar la arquitectura dependerá de la arquitectura que siga tu app y de la biblioteca del mecanismo de ejecución de código asíncrono que uses. Los patrones como MVVP o la arquitectura componible te ayudarán a reutilizar tus modelos de vista y parte de la lógica de la IU sin acoplar la implementación a dependencias de UIKit que no puedes usar con Wasm. Es posible que RXSwift y otras bibliotecas no sean compatibles con Wasm, así que tenlo en cuenta porque deberás usar OpenCombine, async/await, y transmisiones en el código de Swift de Goodnotes.
  • Comprime el objeto binario de Wasm con gzip o brotli. Ten en cuenta que el tamaño del objeto binario será bastante grande para las aplicaciones web clásicas.
  • Incluso cuando puedas usar Wasm sin la AWP, asegúrate de incluir al menos un service worker, aunque tu app web no tenga manifiesto o no quieras que el usuario la instale. El service worker guardará y entregará el objeto binario de Wasm de forma gratuita, además de todos los recursos de la app, para que el usuario no tenga que descargarlos cada vez que abra tu proyecto.
  • Ten en cuenta que contratar personal puede ser más difícil de lo esperado. Es posible que debas contratar a desarrolladores web experimentados con algo de experiencia en Swift o desarrolladores con experiencia en la Web. Si pudieras encontrar ingenieros generalistas con algún conocimiento en ambas plataformas, sería genial

Conclusiones

Crear un proyecto web con una pila tecnológica compleja mientras trabajas en un producto lleno de desafíos es una experiencia increíble. Va a ser difícil, pero vale la pena. Goodnotes nunca podría haber lanzado una versión para Windows, Android, ChromeOS y la Web mientras trabajaba en nuevas funciones de la aplicación para iOS sin usar este enfoque. Gracias a esta pila tecnológica y al equipo de ingeniería de Goodnotes, Goodnotes ahora está en todas partes. Además, el equipo está listo para seguir trabajando en los próximos desafíos. Si quieres obtener más información sobre este proyecto, puedes mirar una charla que dio el equipo de Goodnotes en NSEspaña 2023. Asegúrate de probar Goodnotes para la Web.