Des notes partout

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

Ces deux dernières années, l'équipe d'ingénieurs Goodnotes a travaillé sur un projet visant à déployer sur d'autres plates-formes l'application de prise de notes iPad qui a rencontré un fort succès. Cette étude de cas explique comment l'application iPad de l'année 2022 est arrivée sur le Web, ChromeOS, Android et Windows grâce aux technologies Web, et WebAssembly a réutilisé le code Swift sur lequel l'équipe travaille depuis plus de 10 ans.

Logo Goodnotes.

Pourquoi Goodnotes a-t-il été adopté pour le Web, Android et Windows ?

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

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

Sur la base des exigences et de l'expérience de l'équipe d'ingénierie, l'équipe a rapidement conclu que réutiliser le codebase Swift serait la meilleure solution, étant donné qu'il avait déjà été écrit et testé depuis de nombreuses années. Mais pourquoi ne pas simplement transférer l'application iOS/iPad existante vers une autre plate-forme ou technologie comme Flutter ou la multiplateforme Compose ? Pour passer à une nouvelle plate-forme, vous devrez réécrire Goodnotes. Cela peut démarrer une course de développement entre l'application iOS déjà implémentée et une application à créer à partir de zéro nouvelle application, ou impliquer l'arrêt d'un 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 de nouvelles fonctionnalités implémentées par l'équipe iOS pendant que l'équipe multiplate-forme travaillait sur les principes de base des applications et s'efforçait d'atteindre la parité des fonctionnalités.

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

  • Rendu des notes.
  • Synchronisation des documents et des notes
  • Résolution des conflits pour les notes à l'aide de types de données répliquées sans conflit.
  • Analyse de 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.
  • Affichez l'implémentation du modèle pour toutes les couches de l'interface utilisateur.

Tous ces éléments seraient beaucoup plus faciles à implémenter sur 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 livrer sous forme d'applications Windows, Android ou Web.

Pile technologique de Goodnotes

Heureusement, il existait 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 et géré par la communauté. Avec SwiftWasm, l'équipe Goodnotes a pu générer un binaire Wasm en utilisant tout le code Swift déjà implémenté. Ce binaire peut être inclus dans une page Web envoyée en tant que Progressive Web Application pour Android, Windows, ChromeOS et tout autre système d'exploitation.

Séquence de déploiement des Goodnotes en commençant par Chrome, puis sous Windows, puis par Android et d'autres plates-formes comme Linux à la fin, le tout basé sur la PWA.

L'objectif était de lancer Goodnotes en tant que PWA et de pouvoir l'afficher sur chaque plate-forme de téléchargement d'applications. Outre Swift, le langage de programmation déjà utilisé pour iOS et 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 bundler le plus populaire sur le Web.
  • PWA et service workers: les principaux leviers de ce projet, car l'équipe peut proposer notre application hors connexion sous la forme d'une application hors connexion qui fonctionne comme n'importe quelle autre application iOS, que vous pouvez installer à partir de la boutique ou du navigateur.
  • PWABuilder:projet principal utilisé par Goodnotes pour encapsuler la PWA dans un binaire Windows natif afin que l'équipe puisse distribuer notre application à partir du Microsoft Store.
  • Activités Web fiables:il s'agit de la technologie Android la plus importante utilisée par l'entreprise pour distribuer notre PWA en tant qu'application native.

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

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

Captures d'écran de l'application sur mobile et ordinateur montrant les zones de dessin spécifiques 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, les raisons suivantes expliquent que l'équipe d'ingénieurs Goodnotes a estimé que cette approche était la meilleure décision:

  • La réutilisation de plus de 100 000 lignes de code
  • Capacité à poursuivre le développement du produit principal tout en contribuant aux applications multiplates-formes
  • La possibilité d'accéder à toutes les plates-formes dès que possible à l'aide d'un processus de développement itératif.
  • Pouvoir afficher le même document sans dupliquer toute la logique métier et introduire des différences dans nos implémentations
  • Vous bénéficiez de toutes les améliorations de performances apportées en même temps sur toutes les plates-formes (ainsi que de toutes les corrections de bugs implémentées sur chaque plate-forme).

La réutilisation de plus de 100 000 lignes de code et la logique métier mettant en œuvre notre pipeline de rendu étaient fondamentales. En parallèle, 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 d'envoyer quelque chose aux utilisateurs le plus rapidement possible. Goodnotes a commencé par une version en lecture seule du produit, où les utilisateurs pouvaient obtenir n'importe quel document partagé et le lire à partir de n'importe quelle plate-forme. Un simple lien leur permet d'accéder aux notes qu'ils ont écrites sur leur iPad. Phase suivante ajoutée aux fonctionnalités de modification, pour que les versions multiplates-formes soient équivalentes à celles d'iOS.

Deux captures d'écran de l'application symbolisant le passage du produit en 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 aux premières fonctionnalités d'édition et à l'écran d'interface utilisateur où vous pouvez vérifier tous les documents que vous avez créés ou que quelqu'un a partagés avec vous. De plus, grâce à la chaîne d'outils SwiftWasm, les nouvelles fonctionnalités de la plate-forme iOS ont été faciles à transférer vers le projet multiplate-forme. Par exemple, un nouveau type de stylo a été créé et facilement mis en œuvre multiplate-forme en réutilisant des milliers de lignes de code.

La réalisation 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 concernant le développement Web et l'utilisation de WebAssembly et de langages comme Swift.

Obstacles initiaux

Travailler sur ce projet a été très difficile à tous les points de vue. Le premier obstacle que l'équipe a identifié était lié à la chaîne d'outils SwiftWasm. La chaîne d'outils a joué un rôle déterminant 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 (implémentation de vues, de clients API ou de l'accès à la base de données, par exemple) n'était pas réutilisable. L'équipe a donc dû commencer à refactoriser des parties spécifiques de l'application pour pouvoir les réutiliser depuis la solution multiplate-forme. La plupart des demandes d'extraction créées par l'équipe étaient des refactorisations pour éliminer les dépendances afin que l'équipe puisse les remplacer ultérieurement par l'injection de dépendances ou d'autres stratégies similaires. À l'origine, le code iOS combinait une logique métier brute qui pouvait être implémentée dans Wasm avec du code responsable des entrées/sorties et une interface utilisateur qui ne pouvait pas être implémentée dans Wasm, car il n'est pas compatible non plus avec Wasm. Il a donc fallu réimplémenter le code des E/S et de l'UI dans TypeScript une fois que la logique métier Swift était prête à être réutilisée d'une plate-forme à l'autre.

Problèmes de performances résolus

Une fois que Goodnotes a commencé à travailler sur l'éditeur, l'équipe a identifié des problèmes d'édition, et des contraintes technologiques complexes sont entrées dans notre feuille de route. Le premier problème était lié aux performances. JavaScript est un langage à thread unique. Cela signifie qu'il comporte une pile d'appel et un tas de mémoire. Elle exécute le code dans l'ordre et doit terminer l'exécution d'un morceau de code avant de passer au suivant. Il est synchrone, mais peut parfois être nuisible. Par exemple, si une fonction met un certain temps à s'exécuter ou doit attendre quelque chose, elle fige tout en attendant. Et c'est exactement ce que les ingénieurs ont dû résoudre. L'évaluation de certains chemins d'accès spécifiques de notre codebase liés à la couche de rendu ou à d'autres algorithmes complexes était un 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é certaines pour les rendre asynchrones. Elle a également introduit une stratégie de rendement permettant à l'application d'arrêter l'exécution de l'algorithme et de la poursuivre ultérieurement, ce qui permet au navigateur de mettre à jour l'interface utilisateur et d'éviter l'abandon de frames. Cela n'a pas posé 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énieurs devait également migrer une interface utilisateur basée sur des éléments HTML rattachés au DOM vers une interface de document basée sur un canevas en plein écran. Le projet a commencé à afficher toutes les notes et le contenu liés à un document dans la structure DOM à l'aide d'éléments HTML comme n'importe quelle autre page Web. Toutefois, à un moment donné, il est passé à un canevas plein écran pour améliorer les performances sur les appareils d'entrée de gamme en réduisant le temps que le navigateur traite les mises à jour DOM.

Les changements suivants ont été identifiés par l'équipe d'ingénierie comme des éléments qui auraient pu réduire certains des problèmes rencontrés, si elle les avait faits au début du projet.

  • Déchargez davantage le thread principal en utilisant fréquemment des workers Web pour les algorithmes lourds.
  • Utilisez les fonctions exportées et importées au lieu de la bibliothèque d'interopérabilité JS-Swift depuis le début afin de réduire l'impact sur les performances d'une 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 Wasm natives exportées.
  • Assurez-vous que le code autorise l'utilisation de OffscreenCanvas en arrière-plan afin que l'application puisse décharger le thread principal et transférer toute l'utilisation de l'API Canvas à un nœud de calcul Web afin de maximiser les performances des applications lors de l'écriture de notes.
  • Déplacez toute l'exécution liée à 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.

L'éditeur de texte

Un autre problème intéressant concernait 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. Cependant, cette implémentation n'est pas compatible avec SwiftWasm. L'équipe multiplate-forme a donc été obligée de créer un analyseur personnalisé basé sur la grammaire RTF, puis d'implémenter l'expérience d'édition en transformant le RTF en HTML et inversement. Dans le même temps, l'équipe iOS a commencé à travailler sur la nouvelle implémentation de cet outil afin de remplacer l'utilisation du format 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 était 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. Ce problème d'ingénierie a été résolu à l'aide d'une approche centrée sur l'utilisateur : l'équipe devait réécrire une partie du code pour pouvoir afficher le texte afin d'activer la modification de texte dans une deuxième version.

Versions itératives

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, plusieurs mois plus tard, a expédié une toute nouvelle version offrant de nombreuses fonctionnalités d'édition. Pour publier fréquemment des modifications de code en production, l'équipe a décidé d'utiliser largement les flags de fonctionnalité. Pour chaque version, l'équipe peut activer de nouvelles fonctionnalités et publier des modifications de code mettant en œuvre de nouvelles fonctionnalités que l'utilisateur verrait des semaines plus tard. Cependant, il y a quelque chose que l'équipe pense qu'elle aurait pu améliorer ! Il pense que l'introduction d'un système de drapeaux de fonctionnalités dynamiques aurait permis d'accélérer les choses, car il éviterait d'avoir à effectuer un redéploiement pour modifier les valeurs des indicateurs. Cela donnerait à Goodnotes plus de flexibilité et permettrait d'accélérer 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 le fonctionnement hors connexion. La possibilité de modifier vos documents est l'une des fonctionnalités que vous attendez de toute application de ce type. Cependant, il ne s'agit pas d'une fonctionnalité simple, car Goodnotes permet la collaboration. Cela signifie que toutes les modifications effectuées par différents utilisateurs sur différents appareils devraient se retrouver sur chaque appareil 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ées sans conflit, Goodnotes est en mesure de combiner toutes les modifications apportées à un document par n'importe quel utilisateur et de les fusionner sans conflit de fusion. L'utilisation d'IndexedDB et de l'espace de stockage disponible pour les navigateurs Web ont grandement facilité 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 initial d'environ 40 Mo, en raison de la taille du binaire Wasm. Au départ, l'équipe Goodnotes s'appuyait uniquement sur le cache de navigateur standard pour l'app bundle proprement dit et sur la plupart des points de terminaison de l'API qu'elle utilise. Toutefois, avec le recul, elle aurait pu tirer profit de l'API Cache et des service workers, qui étaient plus fiables auparavant. Au départ, l'équipe a renoncé à cette tâche en raison de sa complexité supposée, mais elle a finalement réalisé que Workbox la rendait beaucoup moins effrayante.

Recommandations lors de l'utilisation de Swift sur le Web

Si vous disposez d'une application iOS avec beaucoup de code à réutiliser, préparez-vous car vous êtes sur le point de commencer une aventure incroyable. Il y a quelques conseils que vous pourriez trouver intéressants avant de commencer.

  • Vérifiez le code que vous souhaitez réutiliser. Si la logique métier de votre application est mise en œuvre côté serveur, vous souhaiterez probablement réutiliser le code de l'interface utilisateur. Wasm ne vous aidera alors pas. L'équipe a brièvement examiné Tokamak, un framework compatible avec SwiftUI pour la création d'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.
  • Vérifiez que votre codebase Swift est prêt. Les modèles de conception logicielle pour la couche d'interface utilisateur ou des architectures spécifiques créent une forte séparation entre votre logique d'UI et votre logique métier, car vous ne pourrez pas réutiliser l'implémentation de la couche d'UI. Une architecture propre ou des 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 de mise en œuvre sont définis comme des abstractions et où le principe d'inversion des dépendances est fortement utilisé.
  • Wasm ne fournit pas de code d'interface utilisateur. Par conséquent, choisissez le framework d'interface utilisateur que vous souhaitez utiliser pour le Web.
  • JSKit vous aidera à intégrer votre code Swift avec JavaScript, mais gardez à l'esprit que si vous utilisez un hotpath, le passage du pont JS-Swift peut s'avérer 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 dynamique de membres dans Swift, un joyau caché !).
  • La réutilisation de votre architecture dépend de l'architecture de 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 une architecture modulable vous aideront à réutiliser vos modèles de vue et une partie de la logique d'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-le donc à 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 brotli. Gardez à l'esprit que la taille du binaire est assez importante pour les applications Web classiques.
  • Même si vous pouvez utiliser Wasm sans la PWA, veillez à 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 ainsi que toutes les ressources de l'application. L'utilisateur n'a donc pas besoin de les télécharger chaque fois qu'il ouvre votre projet.
  • Gardez à l'esprit 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 plateformes, ce serait génial

Conclusions

Créer un projet Web à l'aide d'une pile technologique complexe tout en travaillant sur un produit comportant de nombreux défis est une expérience incroyable. Ça va être dur, mais vraiment ça en vaut la peine. Sans cette approche, Goodnotes n'aurait jamais pu publier de version pour Windows, Android, ChromeOS et le Web tout en travaillant sur de nouvelles fonctionnalités pour l'application iOS. Grâce à cette pile technologique et à l'équipe d'ingénieurs de Goodnotes, Goodnotes est désormais partout et l'équipe est prête à continuer à travailler sur 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'oubliez pas d'essayer Goodnotes pour le Web.