Boas notas em todos os lugares

Imagem de marketing do Goodnotes com uma mulher usando o produto em um iPad.

Nos últimos dois anos, a equipe de engenharia da Goodnotes trabalha em um projeto para levar o sucesso do app de anotações para iPad a outras plataformas. Este estudo de caso aborda como o app para iPad de 2022 chegou à Web, ao ChromeOS, ao Android e ao Windows com tecnologias da Web e o WebAssembly reutilizando o mesmo código Swift em que a equipe trabalha há mais de dez anos.

Logotipo da Goodnotes.

Por que a Goodnotes chegou à Web, ao Android e ao Windows

Em 2021, o Goodnotes só estava disponível como um app para iOS e iPad. A equipe de engenharia da Goodnotes aceitou um grande desafio técnico: criar uma nova versão do Goodnotes, mas para outros sistemas operacionais e plataformas. O produto precisa ser totalmente compatível e renderizar as mesmas notas do aplicativo iOS. Qualquer anotação feita em um PDF ou imagem anexada precisa ser equivalente e mostrar os mesmos traços que o app para iOS mostra. Qualquer traço adicionado precisa ser equivalente àquele que os usuários do iOS podem criar, independentemente da ferramenta que o usuário estava usando, por exemplo, caneta, marcador, caneta-tinteiro, formas ou borracha.

Visualização do app Goodnotes com desenhos e notas escritas à mão.

Com base nos requisitos e na experiência da equipe de engenharia, a equipe concluiu rapidamente que a reutilização da base de código do Swift seria a melhor opção, já que ela já foi escrita e bem testada ao longo de muitos anos. Mas por que não apenas fazer a portabilidade do aplicativo iOS/iPad já existente para outra plataforma ou tecnologia, como o Flutter ou o Compose Multiplatform? Mudar para uma nova plataforma envolveria reescrever o Goodnotes. Isso pode iniciar uma corrida de desenvolvimento entre o aplicativo iOS já implementado e um aplicativo a ser criado sem um aplicativo novo, ou pode envolver a interrupção de um novo desenvolvimento no aplicativo existente enquanto a nova base de código é atualizada. Se a Goodnotes pudesse reutilizar o código Swift, a equipe poderia se beneficiar dos novos recursos implementados pela equipe do iOS enquanto a equipe multiplataforma trabalhava nos princípios básicos do app e alcançava a paridade de recursos.

O produto já havia resolvido vários desafios interessantes para o iOS adicionar recursos como:

  • Renderização das anotações.
  • Sincronização de documentos e notas.
  • Resolução de conflitos de anotações usando tipos de dados replicados sem conflitos.
  • Análise de dados para avaliação de modelos de IA.
  • Pesquisa de conteúdo e indexação de documentos.
  • Experiência e animações de rolagem personalizadas.
  • Implementação do modelo de visualização para todas as camadas da interface.

Seria muito mais fácil implementar todos eles em outras plataformas se a equipe de engenharia pudesse fazer com que a base de código do iOS já estivesse funcionando em aplicativos iOS e iPad e executá-la como parte de um projeto que a Goodnotes pudesse ser enviada como aplicativos para Windows, Android ou Web.

Conjunto de tecnologias da Goodnotes

Felizmente, havia uma maneira de reutilizar o código Swift existente na Web: o WebAssembly (Wasm). A Goodnotes criou um protótipo usando o Wasm com o projeto de código aberto e mantido pela comunidade, o SwiftWasm. Com o SwiftWasm, a equipe do Goodnotes poderia gerar um binário Wasm usando todo o código Swift já implementado. Esse binário pode ser incluído em uma página da Web enviada como um Progressive Web Application para Android, Windows, ChromeOS e todos os outros sistemas operacionais.

Sequência de lançamento do Goodnotes começando com o Chrome, seguido pelo Windows, seguido pelo Android e por outras plataformas, como o Linux, no final, tudo com base no PWA.

O objetivo era lançar o Goodnotes como um PWA e poder listá-lo nas lojas de todas as plataformas. Além do Swift, a linguagem de programação já usada para iOS, e do WebAssembly, usado para executar o código Swift na Web, o projeto usou as seguintes tecnologias:

  • TypeScript::a linguagem de programação mais usada para tecnologias da Web.
  • React e webpack:o framework e o bundler mais usado na Web.
  • PWA e service workers: grandes capacitadores para esse projeto porque a equipe poderia enviar nosso aplicativo como um aplicativo off-line que funciona como qualquer outro aplicativo para iOS e você pode instalá-lo da loja ou do próprio navegador.
  • PWABuilder: o projeto principal que a Goodnotes usa para unir o PWA em um binário nativo do Windows: para que a equipe possa distribuir o app na Microsoft Store.
  • Atividades confiáveis na Web:a tecnologia Android mais importante que a empresa usa para distribuir nosso PWA como um aplicativo nativo em segundo plano.

Pilha de tecnologias da Goodnotes que consiste em Swift, Wasm, React e PWA.

A figura a seguir mostra o que é implementado usando TypeScript e React clássicos e o que é implementado usando SwiftWasm e JavaScript baunilha, Swift e WebAssembly. Essa parte do projeto usa o JSKit, uma biblioteca de interoperabilidade JavaScript para Swift e WebAssembly que a equipe usa para processar o DOM na tela do editor do nosso código Swift quando necessário, ou até usar algumas APIs específicas do navegador.

Capturas de tela do app em dispositivos móveis e computadores mostrando as áreas de desenho específicas sendo impulsionadas pelo Wasm e as áreas da interface geradas pelo React.

Por que usar o Wasm e a Web?

Mesmo que a Apple não tenha suporte oficial ao Wasm, a equipe de engenharia da Goodnotes considerou que essa abordagem foi a melhor escolha pelos seguintes motivos:

  • A reutilização de mais de 100 mil linhas de código.
  • A capacidade de continuar o desenvolvimento no produto principal e, ao mesmo tempo, contribuir para os apps multiplataforma.
  • O poder de chegar a todas as plataformas o mais rápido possível usando um processo de desenvolvimento iterativo.
  • Ter controle para renderizar o mesmo documento sem duplicar toda a lógica de negócios e introduzir diferenças nas nossas implementações.
  • Aproveitando todas as melhorias de desempenho feitas em todas as plataformas ao mesmo tempo, além de todas as correções de bugs implementadas em todas as plataformas.

A reutilização de mais de 100 mil linhas de código e a lógica de negócios que implementava nosso pipeline de renderização foram fundamentais. Ao mesmo tempo, tornar o código Swift compatível com outros conjuntos de ferramentas permite que eles reutilizem esse código em diferentes plataformas no futuro, se necessário.

Desenvolvimento iterativo de produtos

A equipe adotou uma abordagem iterativa para levar algo aos usuários o mais rápido possível. A Goodnotes começou com uma versão somente leitura do produto, em que os usuários podiam acessar qualquer documento compartilhado e lê-lo em qualquer plataforma. Apenas com um link, eles poderiam acessar e ler as mesmas notas que escreveram no iPad. A fase seguinte adicionou recursos de edição para tornar as versões multiplataforma equivalentes à do iOS.

Duas capturas de tela do app simbolizando a mudança do produto somente leitura para o completo.

A primeira versão do produto somente leitura levou seis meses para ser desenvolvida. Os nove meses seguintes foram dedicados ao primeiro grupo de recursos de edição e à tela da interface, em que é possível verificar todos os documentos que você criou ou alguém compartilhou com você. Além disso, foi fácil transferir novos recursos da plataforma iOS para o projeto multiplataforma graças ao conjunto de ferramentas SwiftWasm. Como exemplo, um novo tipo de caneta foi criado e facilmente implementado em várias plataformas reutilizando milhares de linhas de código.

A criação deste projeto foi uma experiência incrível, e a Goodnotes aprendeu muito com ela. É por isso que as seções a seguir se concentrarão em pontos técnicos interessantes sobre o desenvolvimento da Web, o uso do WebAssembly e linguagens como o Swift.

Obstáculos iniciais

Trabalhar neste projeto foi muito desafiador, de muitos pontos de vista diferentes. O primeiro obstáculo encontrado pela equipe estava relacionado à cadeia de ferramentas SwiftWasm. O conjunto de ferramentas foi um grande capacitador para a equipe, mas nem todo o código do iOS era compatível com o Wasm. Por exemplo, o código relacionado a E/S ou IU, como a implementação de visualizações, clientes da API ou acesso ao banco de dados, não era reutilizável, então a equipe precisou começar a refatorar partes específicas do app para poder reutilizá-las da solução multiplataforma. A maioria das PRs que a equipe criou era refatorada para abstrair dependências para que a equipe pudesse substituí-las mais tarde usando injeção de dependência ou outras estratégias semelhantes. Originalmente, o código do iOS misturava a lógica de negócios bruta que poderia ser implementada no Wasm com código responsável por entrada/saída e interface do usuário que não podia ser implementado no Wasm porque o Wasm também não era compatível. Portanto, o E/S e o código da IU precisavam ser reimplementados no TypeScript quando a lógica de negócios do Swift estava pronta para ser reutilizada entre as plataformas.

Problemas de desempenho resolvidos

Quando a Goodnotes começou a trabalhar no editor, a equipe identificou alguns problemas com a experiência de edição, e desafios tecnológicos desafiadores entraram no nosso roteiro. O primeiro problema estava relacionado à performance. O JavaScript é uma linguagem de thread único. Isso significa que ele tem uma pilha de chamadas e uma pilha de memória. Ela executa o código em ordem e precisa terminar de executar uma parte do código antes de passar para o próximo. Ele é síncrono, mas às vezes pode ser prejudicial. Por exemplo, se uma função demorar um pouco para ser executada ou tiver que esperar por algo, ela congela tudo enquanto isso. E isso é exatamente o que os engenheiros tinham que resolver. Avaliar alguns caminhos específicos na nossa base de código relacionados à camada de renderização ou a outros algoritmos complexos era um problema para a equipe, porque esses algoritmos eram síncronos, e a execução deles estava bloqueando a linha de execução principal. A equipe do Goodnotes reescreveu-as para torná-las mais rápidas e refatorou algumas delas para torná-las assíncronas. Ela também introduziu uma estratégia de rendimento para que o app pudesse interromper a execução do algoritmo e continuar mais tarde, permitindo que o navegador atualizasse a interface e evitasse o descarte de frames. Isso não foi um problema para o app iOS, porque ele pode usar linhas de execução e avaliar esses algoritmos em segundo plano enquanto a linha de execução principal do iOS atualiza a interface do usuário.

Outra solução que a equipe de engenharia teve que resolver era migrar uma interface baseada em elementos HTML anexados ao DOM para uma interface de documento baseada em uma tela de tela cheia. O projeto começou a mostrar todas as notas e o conteúdo relacionado a um documento como parte da estrutura do DOM usando elementos HTML, como qualquer outra página da Web faria, mas em algum momento migrou para uma tela de tela cheia para melhorar o desempenho em dispositivos mais simples, reduzindo o tempo que o navegador está trabalhando em atualizações do DOM.

As seguintes alterações foram identificadas pela equipe de engenharia como coisas que poderiam ter reduzido alguns dos problemas encontrados, se tivessem feito isso no início do projeto.

  • Descarregue mais a linha de execução principal usando workers da Web com frequência para algoritmos pesados.
  • Use as funções exportadas e importadas em vez da biblioteca de interoperabilidade JS-Swift desde o início para reduzir o impacto no desempenho ao sair do contexto do Wasm. Essa biblioteca de interoperabilidade do JavaScript é útil para ter acesso ao DOM ou ao navegador, mas é mais lenta do que as funções nativas exportadas pelo Wasm.
  • Verifique se o código permite o uso de OffscreenCanvas em segundo plano para que o app possa descarregar a linha de execução principal e mover todo o uso da API Canvas para um worker da Web, maximizando o desempenho dos aplicativos ao escrever anotações.
  • Mova toda a execução relacionada ao Wasm para um worker da Web ou até mesmo um pool de workers da Web para que o app possa reduzir a carga de trabalho da linha de execução principal.

O editor de texto

Outro problema interessante estava relacionado a uma ferramenta específica, o editor de texto. A implementação dessa ferramenta no iOS é baseada no NSAttributedString, um pequeno conjunto de ferramentas que usa RTF em segundo plano. No entanto, essa implementação não é compatível com o SwiftWasm. Por isso, a equipe multiplataforma foi forçada a criar um analisador personalizado com base na gramática RTF e depois implementar a experiência de edição transformando RTF em HTML e vice-versa. Enquanto isso, a equipe do iOS começou a trabalhar na nova implementação dessa ferramenta substituindo o uso de RTF por um modelo personalizado. Assim, o app pode representar textos estilizados de maneira simples para todas as plataformas que compartilham o mesmo código Swift.

O editor de texto do Goodnotes.

Esse desafio foi um dos pontos mais interessantes do roteiro do projeto, porque foi resolvido de forma iterativa com base nas necessidades do usuário. Foi um problema de engenharia resolvido usando uma abordagem focada no usuário, em que a equipe precisava reescrever parte do código para renderizar texto, permitindo a edição de texto em uma segunda versão.

Lançamentos iterativos

A evolução do projeto nos últimos dois anos foi incrível. A equipe começou a trabalhar em uma versão somente leitura do projeto e, meses depois, enviou uma nova versão com muitos recursos de edição. Para lançar mudanças de código para produção com frequência, a equipe decidiu usar extensivamente as sinalizações de recursos. Para cada versão, a equipe poderia ativar novos recursos e também alterações no código de lançamento, implementando novos recursos que o usuário veria semanas depois. No entanto, há algo que a equipe acha que poderia ter melhorado! Eles acreditam que a introdução de um sistema de sinalização de recursos dinâmicos teria ajudado a acelerar as coisas, já que eliminaria a necessidade de uma reimplantação para alterar os valores das sinalizações. Isso daria mais flexibilidade e também aceleraria a implantação do novo recurso, porque o Goodnotes não precisaria vincular a implantação do projeto à versão do produto.

Trabalho off-line

Um dos principais recursos em que a equipe trabalhou foi o suporte off-line. A capacidade de editar e modificar seus documentos é um recurso esperado de qualquer aplicativo como esse. No entanto, esse não é um recurso simples porque o Goodnotes oferece suporte à colaboração. Isso significa que todas as mudanças feitas por diferentes usuários em dispositivos diferentes vão ser aplicadas a todos os dispositivos, sem que os usuários precisem resolver conflitos. A Goodnotes resolveu esse problema há muito tempo usando CRDTs em segundo plano. Graças a esses tipos de dados replicados sem conflitos, o Goodnotes consegue combinar todas as alterações feitas em qualquer documento por qualquer usuário e mesclar as alterações sem qualquer conflito de mesclagem. O uso do IndexedDB e o armazenamento disponível para navegadores da Web foram essenciais para a experiência off-line colaborativa na Web.

O app Goodnotes funciona off-line.

Além disso, abrir o app da Web Goodnotes resulta em um custo inicial de download inicial de cerca de 40 MB devido ao tamanho do binário Wasm. Inicialmente, a equipe do Goodnotes dependia apenas do cache normal do navegador para o próprio pacote de apps e da maioria dos endpoints de API que usa, mas, por isso, poderia ter aproveitado anteriormente a API Cache e os service workers mais confiáveis. A equipe originalmente escapou dessa tarefa devido à complexidade presumida, mas, no final, percebeu que o Workbox a tornava muito menos assustadora.

Recomendações ao usar o Swift na Web

Se você tem um aplicativo iOS com muito código que quer reutilizar, prepare-se, porque você está prestes a iniciar uma jornada incrível. Há algumas dicas que podem ser interessantes antes de você começar.

  • Selecione o código que você quer reutilizar. Se a lógica de negócios do app estiver implementada no lado do servidor, é provável que você queira reutilizar o código da interface. O Wasm não ajudará você com isso. A equipe analisou rapidamente o Tokamak, um framework compatível com o SwiftUI para a criação de apps de navegador com o WebAssembly, mas ele não estava maduro o suficiente para as necessidades do app. No entanto, se o app tiver uma lógica de negócios ou algoritmos fortes implementados como parte do código do cliente, o Wasm será seu melhor amigo.
  • Verifique se sua base de código Swift está pronta. Os padrões de design de software para a camada da interface ou arquiteturas específicas, criando uma forte separação entre a lógica da interface e a lógica de negócios, serão muito úteis, porque não será possível reutilizar a implementação da camada da interface. A arquitetura limpa ou os princípios de arquitetura hexagonal também serão fundamentais, porque você precisará injetar e fornecer dependências para todo o código relacionado a E/S. Isso será muito mais fácil se você seguir essas arquiteturas em que os detalhes de implementação são definidos como abstrações e o princípio de inversão de dependência é muito usado.
  • O Wasm não fornece o código da interface. Portanto, escolha o framework da interface que você quer usar para a Web.
  • O JSKit vai ajudar você a integrar seu código Swift com o JavaScript. No entanto, se você tiver um atalho, atravessar a ponte JS-Swift pode ser caro e você terá que substituí-lo por funções exportadas. Saiba mais sobre como o JSKit funciona nos bastidores na documentação oficial e na postagem Dynamic Member Lookup in Swift, a good gem!.
  • A reutilização da sua arquitetura depende da arquitetura que seu app segue e da biblioteca do mecanismo de execução de código assíncrono que você usa. Padrões como MVVP ou arquitetura combinável vão ajudar você a reutilizar seus modelos de visualização e parte da lógica da interface sem vincular a implementação a dependências UIKit que não podem ser usadas com o Wasm. O RXSwift e outras bibliotecas podem não ser compatíveis com o Wasm. Lembre-se disso, porque você precisará usar OpenCombine, async/await e streams no código Swift do Goodnotes.
  • Compacte o binário Wasm usando gzip ou brotli. Lembre-se de que o tamanho do binário será muito grande para aplicativos da Web clássicos.
  • Mesmo quando for possível usar o Wasm sem o PWA, inclua pelo menos um service worker, mesmo que o app da Web não tenha um manifesto ou você não queira que o usuário o instale. O service worker salvará e disponibilizará o binário Wasm sem custo financeiro e todos os recursos do app para que o usuário não precise fazer o download deles todas as vezes que abrir seu projeto.
  • Tenha em mente que a contratação pode ser mais difícil do que o esperado. Talvez seja necessário contratar desenvolvedores da Web fortes com alguma experiência em Swift ou desenvolvedores avançados do Swift com alguma experiência na Web. Se você conseguir encontrar engenheiros generalistas com conhecimento em ambas as plataformas,

Conclusões

Criar um projeto da Web usando um conjunto de tecnologias complexo enquanto trabalha em um produto cheio de desafios é uma experiência incrível. Vai ser difícil, mas vale a pena. A Goodnotes nunca poderia ter lançado uma versão para Windows, Android, ChromeOS e Web enquanto trabalhava em novos recursos para o aplicativo iOS sem usar essa abordagem. Graças ao conjunto de tecnologias e à equipe de engenharia da Goodnotes, a Goodnotes agora está em todos os lugares, e a equipe está pronta para continuar trabalhando nos próximos desafios. Se você quiser saber mais sobre esse projeto, assista a uma palestra que a equipe da Goodnotes fez na NSEspanha 2023 (em inglês). Teste o Goodnotes para a Web.