Boas notas em todos os lugares

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

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

Logotipo do Goodnotes.

Por que o 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 com o aplicativo iOS e renderizar as mesmas notas. Qualquer nota feita em cima de um PDF ou qualquer imagem anexada precisa ser equivalente e mostrar os mesmos traços que o app para iOS mostra. Qualquer traço adicionado precisa ser equivalente ao que os usuários do iOS podem criar, independente da ferramenta que o usuário estava usando, por exemplo, caneta, marcador, caneta tinteiro, formas ou borracha.

Prévia do app Goodnotes com anotações manuscritas e esboços.

Com base nos requisitos e na experiência da equipe de engenharia, a equipe concluiu rapidamente que reutilizar a base de código Swift seria a melhor ação a ser tomada, já que ela já foi escrita e bem testada ao longo de muitos anos. Mas por que não portar o aplicativo iOS/iPad já existente para outra plataforma ou tecnologia, como o Flutter ou o Compose Multiplatform? A mudança para uma nova plataforma envolve a reescrita do Goodnotes. Isso pode iniciar uma corrida de desenvolvimento entre o aplicativo iOS já implementado e um novo aplicativo a ser criado do zero ou envolver a interrupção do 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 fundamentos do app e alcançava a paridade de recursos.

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

  • Renderização de notas.
  • Sincronização de documentos e anotações.
  • Resolução de conflitos para notas 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 de rolagem e animações personalizadas.
  • Confira a implementação do modelo para todas as camadas da interface.

Seria muito mais fácil implementar tudo isso em outras plataformas se a equipe de engenharia conseguisse fazer a base de código do iOS funcionar para aplicativos iOS e iPad e executá-la como parte de um projeto que o Goodnotes pudesse enviar como aplicativos para Windows, Android ou Web.

Conjunto de tecnologias da Goodnotes

Felizmente, havia uma maneira de reutilizar o código Swift 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 SwiftWasm. Com o SwiftWasm, a equipe da Goodnotes pode 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 aplicativo da Web progressivo para Android, Windows, ChromeOS e todos os outros sistemas operacionais.

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

O objetivo era lançar o Goodnotes como um PWA e listá-lo na loja de cada plataforma. Além do Swift, a linguagem de programação já usada para iOS, e do WebAssembly, usado para executar 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 conhecidos da Web.
  • PWA e service workers:grandes facilitadores para esse projeto, porque a equipe poderia enviar nosso app como um aplicativo off-line que funciona como qualquer outro app iOS e pode ser instalado na loja ou no próprio navegador.
  • PWABuilder:o projeto principal que a Goodnotes usa para agrupar a 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 nossa PWA como um aplicativo nativo.

A pilha de tecnologia da Goodnotes consiste em Swift, Wasm, React e PWA.

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

Capturas de tela do app em dispositivos móveis e computadores mostrando as áreas de exibição específicas sendo controladas pelo Wasm e as áreas da interface controladas pelo React.

Por que usar o Wasm e a Web?

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

  • Reutilização de mais de 100 mil linhas de código.
  • A capacidade de continuar o desenvolvimento do produto principal e 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 implementações.
  • Aproveitar todas as melhorias de desempenho feitas em todas as plataformas ao mesmo tempo (e todas as correções de bugs implementadas em todas as plataformas).

A reutilização de mais de 100 mil linhas de código e da lógica de negócios que implementa nosso pipeline de renderização foi fundamental. Ao mesmo tempo, tornar o código Swift compatível com outras cadeias de ferramentas permite que ele seja reutilizado em diferentes plataformas no futuro, se necessário.

Desenvolvimento iterativo de produtos

A equipe adotou uma abordagem iterativa para disponibilizar algo aos usuários o mais rápido possível. O Goodnotes começou com uma versão somente leitura do produto, em que os usuários podiam acessar qualquer documento compartilhado e ler em qualquer plataforma. Com apenas um link, eles podem acessar e ler as mesmas anotações que escreveram no iPad. A próxima fase adicionou recursos de edição para tornar as versões multiplataforma equivalentes à do iOS.

Duas capturas de tela do app que simbolizam a mudança de somente leitura para o produto com todos os recursos.

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

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

Obstáculos iniciais

Trabalhar nesse projeto foi muito desafiador de vários pontos de vista. O primeiro obstáculo encontrado pela equipe estava relacionado à cadeia de ferramentas SwiftWasm. A cadeia de ferramentas foi um grande facilitador para a equipe, mas nem todo 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 de API ou acesso ao banco de dados, não era reutilizável. Por isso, a equipe precisava começar a refatorar partes específicas do app para poder reutilizá-las na solução multiplataforma. A maioria das PRs criadas pela equipe eram refatorações para dependências abstratas, para que a equipe pudesse substituí-las mais tarde usando injeção de dependência ou outras estratégias semelhantes. O código do iOS originalmente misturava a lógica de negócios bruta que podia ser implementada no Wasm com o código responsável pela entrada/saída e pela interface do usuário que não podia ser implementado no Wasm porque ele não tinha suporte. Portanto, o código de IO e da interface precisava ser reimplementado no TypeScript assim que a lógica de negócios do Swift estivesse pronta para ser reutilizada entre 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 restrições tecnológicas desafiadoras entraram no plano de ação. O primeiro problema estava relacionado ao desempenho. O JavaScript é uma linguagem de linha única. Isso significa que ele tem uma pilha de chamadas e uma pilha de memória. Ele executa o código em ordem e precisa concluir a execução de um trecho de 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 para ser executada ou tiver que esperar por algo, ela congelará tudo nesse meio tempo. E isso é exatamente o que os engenheiros tiveram que resolver. Avaliar alguns caminhos específicos na nossa base de código relacionados à camada de renderização ou outros algoritmos complexos foi um problema para a equipe, porque esses algoritmos eram síncronos e executá-los estava bloqueando a linha de execução principal. A equipe da Goodnotes as reescreveu para torná-las mais rápidas e refatorou algumas para torná-las assíncronas. Eles também introduziram uma estratégia de rendimento para que o app pudesse interromper a execução do algoritmo e continuar depois, permitindo que o navegador atualize a interface e evite a perda de frames. Isso não era um problema para o aplicativo 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 foi 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 DOM usando elementos HTML, como qualquer outra página da Web faria, mas em algum momento migrou para uma tela cheia para melhorar o desempenho em dispositivos de baixo custo, reduzindo o tempo que o navegador está trabalhando em atualizações DOM.

As mudanças a seguir foram identificadas pela equipe de engenharia como coisas que poderiam ter reduzido alguns dos problemas encontrados, se tivessem sido feitas no início do projeto.

  • Use workers da Web com frequência para algoritmos pesados e descarregue a linha de execução principal.
  • Faça uso de funções exportadas e importadas em vez da biblioteca de interoperabilidade JS-Swift desde o início para reduzir o impacto de desempenho ao sair do contexto do Wasm. Essa biblioteca de interoperabilidade do JavaScript é útil para acessar o DOM ou o navegador, mas é mais lenta do que as funções exportadas nativas do Wasm.
  • Verifique se o código permite o uso de OffscreenCanvas no bastidores 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 notas.
  • 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 foi relacionado a uma ferramenta específica, o editor de texto. A implementação do iOS para essa ferramenta é baseada em 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 primeiro 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 para que o app possa representar texto estilizado de maneira amigável 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 cronograma do projeto porque foi resolvido de forma iterativa com base nas necessidades do usuário. Era um problema de engenharia resolvido usando uma abordagem focada no usuário em que a equipe precisava reescrever parte do código para renderizar o texto, permitindo a edição de texto em uma segunda versão.

Versões iterativas

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, lançou uma versão totalmente nova com muitos recursos de edição. Para lançar mudanças de código com frequência na produção, a equipe decidiu usar extensivamente flags de recursos. Em cada lançamento, a equipe podia ativar novos recursos e também lançar mudanças de código que implementavam novos recursos que o usuário conheceria semanas depois. No entanto, a equipe acha que poderia ter melhorado algo. Eles acham que a introdução de um sistema dinâmico de flags de recursos teria ajudado a acelerar as coisas, já que não seria necessário fazer um novo deploy para mudar os valores das flags. Isso daria mais flexibilidade ao Goodnotes e também aceleraria a implantação do novo recurso, porque o Goodnotes não precisaria vincular a implantação do projeto ao lançamento 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 documentos é um recurso que você espera 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 usuários diferentes em dispositivos diferentes precisam ser aplicadas em todos os dispositivos sem que os usuários precisem resolver conflitos. A Goodnotes resolveu esse problema há muito tempo usando CRDTs. Graças a esses tipos de dados replicados sem conflitos, o Goodnotes pode combinar todas as mudanças feitas em qualquer documento por qualquer usuário e mesclar as mudanças sem conflitos de mesclagem. O uso do IndexedDB e do armazenamento disponível para navegadores da Web foi um grande facilitador para a experiência off-line colaborativa na Web.

O app Goodnotes funcionando off-line.

Além disso, a abertura do app da Web da Goodnotes resulta em um custo inicial de download de cerca de 40 MB devido ao tamanho binário do Wasm. Inicialmente, a equipe da Goodnotes dependia apenas do cache do navegador normal para o pacote do app e a maioria dos endpoints de API que eles usavam, mas, em retrospectiva, eles poderiam ter se beneficiado da API Cache mais confiável e dos service workers mais cedo. A equipe originalmente evitou essa tarefa devido à complexidade presumida, mas, no final, percebeu que o Workbox a tornou 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 começar uma jornada incrível. Confira algumas dicas que podem ser úteis antes de começar.

  • Confira o código que você quer reutilizar. Se a lógica de negócios do app for implementada no lado do servidor, é provável que você queira reutilizar o código da interface, e o Wasm não vai ajudar você aqui. A equipe analisou brevemente o Tokamak, um framework compatível com o SwiftUI para criar apps de navegador com o WebAssembly, mas ele não era maduro o suficiente para as necessidades do app. No entanto, se o app tiver uma lógica de negócios ou algoritmos implementados como parte do código do cliente, o Wasm será seu melhor amigo.
  • Confira se a base de código Swift está pronta. Padrões de design de software para a camada de interface ou arquiteturas específicas que criam uma forte separação entre a lógica da interface e a lógica de negócios são muito úteis, porque você não vai poder reutilizar a implementação da camada de interface. A arquitetura limpa ou princípios de arquitetura hexagonal também serão fundamentais, porque você terá que injetar e fornecer dependências para todo o código relacionado à 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 é usado com frequência.
  • O Wasm não fornece código de interface. Portanto, escolha o framework de interface que você quer usar na Web.
  • O JSKit vai ajudar você a integrar seu código Swift com o JavaScript, mas lembre-se de que, se você tiver um caminho de acesso rápido, a ponte JS-Swift pode ser cara e será necessário substituí-la por funções exportadas. Saiba mais sobre como o JSKit funciona na documentação oficial e na postagem Dynamic Member Lookup in Swift, a hidden gem!.
  • A possibilidade de reutilizar a arquitetura depende da arquitetura que o app segue e da biblioteca de mecanismo de execução de código assíncrono usada. 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 acoplar a implementação a dependências do UIKit que não podem ser usadas com o Wasm. O RXSwift e outras bibliotecas podem não ser compatíveis com o Wasm. Portanto, lembre-se disso porque você vai precisar usar OpenCombine, async/await e streams no código Swift do Goodnotes.
  • Compacte o binário Wasm usando gzip ou brotli. O tamanho do binário será bastante grande para aplicativos da Web clássicos.
  • Mesmo quando você pode usar o Wasm sem o PWA, inclua pelo menos um service worker, mesmo que seu app da Web não tenha um manifesto ou você não queira que o usuário o instale. O worker de serviço vai salvar e oferecer o binário Wasm de graça, assim como todos os recursos do app, para que o usuário não precise fazer o download deles toda vez que abrir seu projeto.
  • Lembre-se de que a contratação pode ser mais difícil do que o esperado. Talvez você precise contratar desenvolvedores da Web com experiência em Swift ou desenvolvedores Swift com experiência na Web. Se você encontrar engenheiros generalistas com algum conhecimento sobre as duas plataformas, isso seria ótimo

Conclusões

Criar um projeto da Web usando uma pilha de tecnologia complexa enquanto trabalha em um produto cheio de desafios é uma experiência incrível. Vai ser difícil, mas valerá a pena. O 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 a essa pilha de tecnologia e à equipe de engenharia do Goodnotes, o app 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 da equipe da Goodnotes na NSSpain 2023. Teste o Goodnotes para Web.