Des notes partout

Image marketing de Goodnotes montrant une femme utilisant le produit sur un iPad.

Depuis deux ans, l'équipe d'ingénieurs Goodnotes travaille sur un projet visant à proposer l'application de prise de notes populaire sur iPad sur d'autres plates-formes. Cette étude de cas explique comment l'application iPad de l'année 2022 a été portée sur le Web, ChromeOS, Android et Windows grâce aux technologies Web et WebAssembly, en réutilisant le même code Swift sur lequel l'équipe travaille depuis plus de dix ans.

Logo Goodnotes.

Pourquoi GoodNotes est-il disponible sur le Web, Android et Windows ?

En 2021, Goodnotes n'était disponible que sous la forme d'une application pour iOS et iPad. L'équipe d'ingénieurs de Goodnotes a accepté un énorme défi technique: créer une nouvelle version de Goodnotes, mais pour d'autres systèmes d'exploitation et plates-formes. Le produit doit être entièrement compatible avec l'application iOS et afficher les mêmes notes. Toute note prise sur un PDF ou toute image jointe doit être équivalente et afficher les mêmes traits que l'application iOS. Tout trait ajouté doit être équivalent à celui que les utilisateurs d'iOS peuvent créer, indépendamment de l'outil utilisé (par exemple, stylo, surligneur, stylo plume, formes ou gomme).

Aperçu de l'application Goodnotes avec des notes et des croquis manuscrits.

En fonction des exigences et de l'expérience de l'équipe d'ingénieurs, l'équipe a rapidement conclu que la réutilisation du codebase Swift était la meilleure solution, étant donné qu'il avait déjà été écrit et bien testé pendant de nombreuses années. Mais pourquoi ne pas simplement porter l'application iOS/iPad déjà existante vers une autre plate-forme ou technologie comme Flutter ou Compose Multiplatform ? Passer à une nouvelle plate-forme impliquerait de réécrire GoodNotes. Cela pourrait déclencher une course de développement entre l'application iOS déjà implémentée et une nouvelle application à créer à partir de zéro, ou impliquer l'arrêt du nouveau développement sur l'application existante pendant que le nouveau codebase rattrape son retard. Si Goodnotes pouvait réutiliser le code Swift, l'équipe pourrait bénéficier des nouvelles fonctionnalités implémentées par l'équipe iOS pendant que l'équipe multiplate-forme travaillerait sur les principes fondamentaux de l'application et atteindrait la parité des fonctionnalités.

Le produit avait déjà résolu un certain nombre de défis intéressants pour iOS afin d'ajouter des fonctionnalités telles que:

  • Affichage des notes.
  • Synchronisation des documents et des notes
  • Résolution des conflits pour les notes à l'aide de types de données répliqués sans conflit.
  • Analyse des données pour l'évaluation des modèles d'IA
  • Recherche de contenu et indexation de documents
  • Expérience de défilement et animations personnalisées
  • Implémentation du modèle de vue pour toutes les couches d'interface utilisateur.

Tous seraient beaucoup plus faciles à implémenter pour d'autres plates-formes si l'équipe d'ingénieurs pouvait faire fonctionner le codebase iOS pour les applications iOS et iPad, et l'exécuter dans le cadre d'un projet que Goodnotes pourrait distribuer en tant qu'application Windows, Android ou Web.

Pile technologique de Goodnotes

Heureusement, il existe un moyen de réutiliser le code Swift existant sur le Web : WebAssembly (Wasm). Goodnotes a créé un prototype à l'aide de Wasm avec le projet Open Source SwiftWasm géré par la communauté. Avec SwiftWasm, l'équipe Goodnotes a pu générer un binaire Wasm à l'aide de tout le code Swift déjà implémenté. Ce binaire peut être inclus dans une page Web distribuée en tant que progressive web app pour Android, Windows, ChromeOS et tout autre système d'exploitation.

Séquence de déploiement de Goodnotes commençant par Chrome, puis par Windows, suivi d'Android et d'autres plates-formes comme Linux à la fin, toutes basées sur la PWA.

L'objectif était de lancer Goodnotes en tant que PWA et de pouvoir le lister sur la plate-forme de chaque plate-forme. En plus de Swift, le langage de programmation déjà utilisé pour iOS, et de WebAssembly, utilisé pour exécuter du code Swift sur le Web, le projet a utilisé les technologies suivantes:

  • TypeScript:langage de programmation le plus fréquemment utilisé pour les technologies Web.
  • React et Webpack:framework et outil de compilation les plus populaires pour le Web.
  • PWA et service workers:éléments essentiels de ce projet, car l'équipe a pu distribuer notre application en tant qu'application hors connexion qui fonctionne comme n'importe quelle autre application iOS et que vous pouvez installer depuis le Play Store ou le navigateur lui-même.
  • PWABuilder:projet principal utilisé par Goodnotes pour encapsuler la PWA dans un binaire Windows natif afin que l'équipe puisse distribuer notre application depuis le Microsoft Store.
  • Activités Web fiables:technologie Android la plus importante utilisée par l'entreprise pour distribuer notre PWA en tant qu'application native.

Stack technologique Goodnotes composée de Swift, Wasm, React et PWA.

La figure suivante montre ce qui est implémenté à l'aide de TypeScript et React classiques, et ce qui est implémenté à l'aide de SwiftWasm et de JavaScript, Swift et WebAssembly standards. Cette partie du projet utilise JSKit, une bibliothèque d'interopérabilité JavaScript pour Swift et WebAssembly que l'équipe utilise pour gérer le DOM dans l'écran de l'éditeur à partir de notre code Swift si nécessaire, ou même pour utiliser certaines API spécifiques au navigateur.

Captures d'écran de l'application sur mobile et sur ordinateur, montrant les zones de dessin particulières gérées par Wasm et les zones d'interface utilisateur gérées par React.

Pourquoi utiliser Wasm et le Web ?

Même si Wasm n'est pas officiellement pris en charge par Apple, l'équipe d'ingénierie Goodnotes a estimé que cette approche était la meilleure pour les raisons suivantes:

  • Réutilisation de plus de 100 000 lignes de code
  • La possibilité de poursuivre le développement du produit principal tout en contribuant aux applications multiplates-formes.
  • La possibilité de toucher toutes les plates-formes dès que possible à l'aide d'un processus de développement itératif.
  • Contrôler l'affichage du même document sans dupliquer toute la logique métier et sans créer de différences dans nos implémentations.
  • Profitez de toutes les améliorations de performances effectuées sur toutes les plates-formes en même temps (et de toutes les corrections de bugs implémentées sur toutes les plates-formes).

La réutilisation de plus de 100 000 lignes de code et de la logique métier implémentant notre pipeline de rendu a été fondamentale. En même temps, rendre le code Swift compatible avec d'autres chaînes d'outils leur permet de réutiliser ce code sur différentes plates-formes à l'avenir, si nécessaire.

Développement itératif de produits

L'équipe a adopté une approche itérative afin de proposer quelque chose aux utilisateurs le plus rapidement possible. Goodnotes a commencé avec une version en lecture seule du produit, qui permettait aux utilisateurs d'obtenir n'importe quel document partagé et de le lire depuis n'importe quelle plate-forme. Il lui suffit d'un lien pour accéder aux mêmes notes qu'il a écrites sur son iPad et les lire. La phase suivante a ajouté des fonctionnalités de retouche pour que les versions multiplates-formes soient équivalentes à celle d'iOS.

Deux captures d'écran d'une application symbolisant le passage du mode lecture seule au produit complet.

Le développement de la première version du produit en lecture seule a pris six mois. Les neuf mois suivants ont été consacrés au premier ensemble de fonctionnalités de modification et à l'écran de l'interface utilisateur où vous pouvez consulter tous les documents que vous avez créés ou que quelqu'un a partagés avec vous. De plus, les nouvelles fonctionnalités de la plate-forme iOS ont été faciles à porter vers le projet multiplate-forme grâce à la chaîne d'outils SwiftWasm. Par exemple, un nouveau type de stylet a été créé et facilement implémenté multiplate-forme en réutilisant des milliers de lignes de code.

La création de ce projet a été une expérience incroyable, et Goodnotes en a beaucoup appris. C'est pourquoi les sections suivantes se concentreront sur des points techniques intéressants sur le développement Web, l'utilisation de WebAssembly et des langages tels que Swift.

Obstacles initiaux

Travailler sur ce projet a été très difficile à bien des égards. Le premier obstacle rencontré par l'équipe était lié à la chaîne d'outils SwiftWasm. La chaîne d'outils a été un grand facilitateur pour l'équipe, mais tout le code iOS n'était pas compatible avec Wasm. Par exemple, le code lié aux E/S ou à l'UI (comme l'implémentation de vues, les clients d'API ou l'accès à la base de données) n'était pas réutilisable. L'équipe a donc dû commencer à refactoriser des parties spécifiques de l'application pour pouvoir les réutiliser à partir de la solution multiplate-forme. La plupart des PR créées par l'équipe ont été refactorisées pour extraire les dépendances afin que l'équipe puisse les remplacer ultérieurement à l'aide d'une injection de dépendances ou d'autres stratégies similaires. Le code iOS mélangeait à l'origine une logique métier brute pouvant être implémentée dans Wasm avec le code responsable de l'entrée/sortie et de l'interface utilisateur qui ne pouvait pas être implémenté dans Wasm, car Wasm ne le prenait pas en charge non plus. Le code d'E/S et d'UI a donc dû être réimplémenté en TypeScript une fois que la logique métier Swift était prête à être réutilisée entre les plates-formes.

Résolution des problèmes de performances

Une fois que Goodnotes a commencé à travailler sur l'éditeur, l'équipe a identifié certains problèmes liés à l'expérience de retouche, et des contraintes technologiques difficiles ont été intégrées à notre feuille de route. Le premier problème était lié aux performances. JavaScript est un langage à thread unique. Cela signifie qu'il dispose d'une pile d'appels et d'une mémoire tampon. Il exécute le code dans l'ordre et doit terminer l'exécution d'un fragment de code avant de passer au suivant. Il est synchrone, mais cela peut parfois être dangereux. Par exemple, si l'exécution d'une fonction prend un certain temps ou doit attendre quelque chose, elle fige tout en attendant. C'est exactement ce que les ingénieurs ont dû résoudre. L'évaluation de certains chemins spécifiques de notre codebase liés à la couche de rendu ou à d'autres algorithmes complexes posait problème pour l'équipe, car ces algorithmes étaient synchrones et leur exécution bloquait le thread principal. L'équipe Goodnotes les a réécrits pour les rendre plus rapides et en a refactorisé certains pour les rendre asynchrones. Ils ont également introduit une stratégie de rendement afin que l'application puisse arrêter l'exécution de l'algorithme et la poursuivre plus tard, ce qui permet au navigateur de mettre à jour l'interface utilisateur et d'éviter de perdre des frames. Ce n'était pas un problème pour l'application iOS, car elle peut utiliser des threads et évaluer ces algorithmes en arrière-plan pendant que le thread iOS principal met à jour l'interface utilisateur.

L'équipe d'ingénierie a également dû migrer une UI basée sur des éléments HTML associés au DOM vers une UI de document basée sur un canevas en plein écran. Le projet a commencé à afficher toutes les notes et le contenu associé à un document dans la structure DOM à l'aide d'éléments HTML, comme le ferait n'importe quelle autre page Web. À un moment donné, il a migré vers un canevas en plein écran pour améliorer les performances sur les appareils bas de gamme en réduisant le temps pendant lequel le navigateur travaille sur les mises à jour du DOM.

L'équipe d'ingénierie a identifié les modifications suivantes comme des éléments qui auraient pu réduire certains des problèmes rencontrés, si elle les avait effectuées au début du projet.

  • Déchargez davantage le thread principal en utilisant fréquemment des nœuds de calcul Web pour les algorithmes lourds.
  • Utilisez des fonctions exportées et importées au lieu de la bibliothèque d'interopérabilité JS-Swift dès le début afin de réduire l'impact sur les performances de la sortie du contexte Wasm. Cette bibliothèque d'interopérabilité JavaScript est utile pour accéder au DOM ou au navigateur, mais elle est plus lente que les fonctions natives exportées Wasm.
  • Assurez-vous que le code autorise l'utilisation de OffscreenCanvas en interne afin que l'application puisse décharger le thread principal et déplacer toute utilisation de l'API Canvas vers un worker Web qui maximise les performances des applications lors de la rédaction de notes.
  • Déplacez toutes les exécutions liées à Wasm vers un nœud de calcul Web ou même un pool de nœuds de calcul Web afin que l'application puisse réduire la charge de travail du thread principal.

Éditeur de texte

Un autre problème intéressant était lié à un outil spécifique, l'éditeur de texte. L'implémentation iOS de cet outil est basée sur NSAttributedString, un petit ensemble d'outils utilisant RTF en arrière-plan. Toutefois, cette implémentation n'est pas compatible avec SwiftWasm. L'équipe multiplate-forme a donc été contrainte de créer d'abord un analyseur personnalisé basé sur la grammaire RTF, puis d'implémenter l'expérience de modification en transformant le RTF en HTML et inversement. Pendant ce temps, l'équipe iOS a commencé à travailler sur la nouvelle implémentation de cet outil, en remplaçant l'utilisation de RTF par un modèle personnalisé afin que l'application puisse représenter le texte stylisé de manière conviviale pour toutes les plates-formes partageant le même code Swift.

Éditeur de texte Goodnotes

Ce défi a été l'un des points les plus intéressants de la feuille de route du projet, car il a été résolu de manière itérative en fonction des besoins de l'utilisateur. Il s'agissait d'un problème d'ingénierie résolu à l'aide d'une approche centrée sur l'utilisateur. L'équipe a dû réécrire une partie du code pour pouvoir afficher du texte. Elle a donc activé la modification du texte dans une deuxième version.

Versions itératifs

L'évolution du projet au cours des deux dernières années a été incroyable. L'équipe a commencé à travailler sur une version en lecture seule du projet et, des mois plus tard, a publié une toute nouvelle version avec de nombreuses fonctionnalités de modification. Pour publier fréquemment des modifications de code en production, l'équipe a décidé d'utiliser intensivement des indicateurs de fonctionnalité. Pour chaque version, l'équipe pouvait activer de nouvelles fonctionnalités et publier des modifications de code implémentant de nouvelles fonctionnalités que l'utilisateur verrait des semaines plus tard. Cependant, l'équipe pense qu'elle aurait pu améliorer certains points. Il pense que l'introduction d'un système de flag de fonctionnalité dynamique aurait permis d'accélérer le processus, car il n'aurait pas été nécessaire de redéployer l'application pour modifier les valeurs des indicateurs. Cela donnerait à Goodnotes plus de flexibilité et accélérerait le déploiement de la nouvelle fonctionnalité, car Goodnotes n'aurait pas besoin d'associer le déploiement du projet à la version du produit.

Travail hors connexion

L'une des principales fonctionnalités sur lesquelles l'équipe a travaillé est la compatibilité hors connexion. La possibilité de modifier vos documents est une fonctionnalité que vous attendez de toute application de ce type. Toutefois, il ne s'agit pas d'une fonctionnalité simple, car Goodnotes est compatible avec la collaboration. Cela signifie que toutes les modifications apportées par différents utilisateurs sur différents appareils doivent se retrouver sur tous les appareils sans demander aux utilisateurs de résoudre les conflits. Goodnotes a résolu ce problème il y a longtemps en utilisant des CRDT en arrière-plan. Grâce à ces types de données répliqués sans conflit, Goodnotes peut combiner toutes les modifications apportées à n'importe quel document par n'importe quel utilisateur et fusionner les modifications sans conflit de fusion. L'utilisation d'IndexedDB et de l'espace de stockage disponible pour les navigateurs Web a été un facteur déterminant pour l'expérience collaborative hors connexion sur le Web.

L'application Goodnotes fonctionne hors connexion.

De plus, l'ouverture de l'application Web Goodnotes entraîne un coût de téléchargement initial d'environ 40 Mo en raison de la taille binaire Wasm. Au départ, l'équipe Goodnotes s'appuyait uniquement sur le cache du navigateur standard pour le bundle d'applications lui-même et la plupart des points de terminaison d'API qu'elle utilise. Mais avec le recul, elle aurait pu profiter plus tôt de l'API Cache et des services workers plus fiables. L'équipe a d'abord évité cette tâche en raison de sa complexité supposée, mais a finalement réalisé que Workbox la rendait beaucoup moins effrayante.

Recommandations concernant l'utilisation de Swift sur le Web

Si vous avez une application iOS avec beaucoup de code que vous souhaitez réutiliser, préparez-vous, car vous êtes sur le point de commencer un voyage incroyable. Voici quelques conseils qui pourraient vous intéresser avant de commencer.

  • Vérifiez le code que vous souhaitez réutiliser. Si la logique métier de votre application est implémentée côté serveur, vous aimeriez probablement réutiliser votre code d'interface utilisateur. Wasm ne vous aidera pas dans ce cas. L'équipe a brièvement examiné Tokamak, un framework compatible avec SwiftUI pour créer des applications de navigateur avec WebAssembly, mais il n'était pas assez mature pour les besoins de l'application. Toutefois, si votre application dispose d'une logique métier ou d'algorithmes solides implémentés dans le code client, Wasm sera votre meilleur ami.
  • Assurez-vous que votre codebase Swift est prêt. Les modèles de conception logicielle pour la couche d'UI ou les architectures spécifiques qui créent une séparation forte entre votre logique d'UI et votre logique métier seront très utiles, car vous ne pourrez pas réutiliser l'implémentation de la couche d'UI. L'architecture propre ou les principes d'architecture hexagonale seront également fondamentaux, car vous devrez injecter et fournir des dépendances pour tout le code lié aux E/S. Cela sera beaucoup plus facile si vous suivez ces architectures, où les détails d'implémentation sont définis comme des abstractions et que le principe d'inversion des dépendances est largement utilisé.
  • Wasm ne fournit pas de code d'interface utilisateur. Déterminez donc le framework d'UI que vous souhaitez utiliser pour le Web.
  • JSKit vous aidera à intégrer votre code Swift à JavaScript, mais gardez à l'esprit que si vous disposez d'un chemin d'accès rapide, le pont JS-Swift peut être coûteux et vous devrez le remplacer par des fonctions exportées. Pour en savoir plus sur le fonctionnement de JSKit, consultez la documentation officielle et l'article Dynamic Member Lookup in Swift, a hidden gem! (Recherche de membres dynamique en Swift, un joyau caché).
  • La possibilité de réutiliser votre architecture dépend de l'architecture suivie par votre application et de la bibliothèque de mécanisme d'exécution de code asynchrone que vous utilisez. Des modèles tels que MVVP ou l'architecture composable vous aideront à réutiliser vos modèles de vue et une partie de la logique de l'UI sans associer l'implémentation aux dépendances UIKit que vous ne pouvez pas utiliser avec Wasm. RXSwift et d'autres bibliothèques peuvent ne pas être compatibles avec Wasm. Gardez cela à l'esprit, car vous devrez utiliser OpenCombine, async/await et les flux dans le code Swift de Goodnotes.
  • Compressez le binaire Wasm à l'aide de gzip ou de brotli. N'oubliez pas que la taille du binaire sera assez importante pour les applications Web classiques.
  • Même si vous pouvez utiliser Wasm sans la PWA, assurez-vous d'inclure au moins un service worker, même si votre application Web ne comporte pas de fichier manifeste ou si vous ne souhaitez pas que l'utilisateur l'installe. Le service worker enregistre et diffuse sans frais le binaire Wasm et toutes les ressources de l'application afin que l'utilisateur n'ait pas besoin de les télécharger à chaque fois qu'il ouvre votre projet.
  • N'oubliez pas que le recrutement peut être plus difficile que prévu. Vous devrez peut-être embaucher des développeurs Web expérimentés avec une certaine expérience de Swift ou des développeurs Swift expérimentés avec une certaine expérience du Web. Si vous pouvez trouver des ingénieurs généralistes ayant des connaissances sur les deux plates-formes, ce serait génial.

Conclusions

Créer un projet Web à l'aide d'une pile technologique complexe tout en travaillant sur un produit plein de défis est une expérience incroyable. Ce ne sera pas facile, mais cela en vaudra la peine. Goodnotes n'aurait jamais pu publier une version pour Windows, Android, ChromeOS et le Web tout en travaillant sur de nouvelles fonctionnalités pour l'application iOS sans cette approche. Grâce à cette pile technologique et à l'équipe d'ingénieurs de Goodnotes, Goodnotes est désormais disponible partout. L'équipe est prête à relever les prochains défis. Pour en savoir plus sur ce projet, vous pouvez regarder une présentation de l'équipe Goodnotes lors de la conférence NSSpain 2023. N'hésitez pas à essayer Goodnotes pour le Web !