Negli ultimi due anni, il team di ingegneria di Goodnotes ha lavorato a un progetto per portare la popolare app di annotazione per iPad su altre piattaforme. Questo caso di studio illustra come l'app per iPad dell'anno 2022 è arrivata su web, ChromeOS, Android e Windows grazie alle tecnologie web e WebAssembly, riutilizzando lo stesso codice Swift su cui il team lavora da più di dieci anni.
Perché Goodnotes è disponibile su web, Android e Windows
Nel 2021, Goodnotes era disponibile solo come app per iOS e iPad. Il team di ingegneria di Goodnotes ha accettato una grande sfida tecnica: creare una nuova versione di Goodnotes, ma per piattaforme e sistemi operativi aggiuntivi. Il prodotto deve essere completamente compatibile con l'applicazione per iOS e visualizzare le stesse note. Qualsiasi nota acquisita sopra un PDF o qualsiasi immagine allegata deve essere equivalente e mostrare gli stessi tratti dell'app per iOS. Qualsiasi tratto aggiunto deve essere equivalente a quello che possono creare gli utenti iOS, indipendentemente dallo strumento utilizzato, ad esempio penna, evidenziatore, stilo, forme o gomma.
In base ai requisiti e all'esperienza del team tecnico, il team ha rapidamente concluso che il riutilizzo della base di codice Swift sarebbe stata la migliore linea di azione, dato che era già stata scritta e testata a fondo per molti anni. Ma perché non eseguire il porting dell'applicazione iOS/iPad già esistente su un'altra piattaforma o tecnologia come Flutter o Compose Multiplatform? Il passaggio a una nuova piattaforma implicherebbe una riscrittura di Goodnotes. In questo modo, potresti avviare una gara di sviluppo tra l'applicazione per iOS già implementata e una nuova applicazione da creare da zero oppure potresti dover interrompere il nuovo sviluppo dell'applicazione esistente mentre la nuova codebase viene aggiornata. Se Goodnotes potesse riutilizzare il codice Swift, il team potrebbe trarre vantaggio dalle nuove funzionalità implementate dal team iOS mentre il team multipiattaforma lavorava sulle basi dell'app e raggiungere la parità di funzionalità.
Il prodotto aveva già risolto una serie di problemi interessanti per iOS per aggiungere funzionalità come:
- Visualizzazione delle note.
- Sincronizzazione di documenti e note.
- Risoluzione dei conflitti per le note che utilizzano tipi di dati replicati senza conflitti.
- Analisi dei dati per la valutazione dei modelli di IA.
- Ricerca di contenuti e indicizzazione dei documenti.
- Esperienza di scorrimento e animazioni personalizzate.
- Visualizza l'implementazione del modello per tutti i livelli dell'interfaccia utente.
Tutti questi elementi sarebbero molto più facili da implementare per altre piattaforme se il team di ingegneria potesse far funzionare il codice di base di iOS già per le applicazioni iOS e iPad ed eseguirlo nell'ambito di un progetto che Goodnotes potrebbe distribuire come applicazioni web, Android o Windows.
Stack tecnologico di Goodnotes
Fortunatamente, esisteva un modo per riutilizzare il codice Swift esistente sul web: WebAssembly (Wasm). Goodnotes ha creato un prototipo utilizzando Wasm con il progetto open source e gestito dalla community SwiftWasm. Con SwiftWasm, il team di Goodnotes ha potuto generare un file binario Wasm utilizzando tutto il codice Swift già implementato. Questo file binario potrebbe essere incluso in una pagina web distribuita come app web progressive per Android, Windows, ChromeOS e ogni altro sistema operativo.
L'obiettivo era rilasciare Goodnotes come PWA e poterlo mostrare nello store di ogni piattaforma. Oltre a Swift, il linguaggio di programmazione già utilizzato per iOS, e WebAssembly utilizzato per eseguire il codice Swift sul web, il progetto ha utilizzato le seguenti tecnologie:
- TypeScript:il linguaggio di programmazione più utilizzato per le tecnologie web.
- React e webpack:il framework e il bundler più utilizzati per il web.
- PWA e service worker: fattori determinanti per questo progetto perché il team poteva distribuire la nostra app come applicazione offline che funziona come qualsiasi altra app per iOS e che puoi installare dallo store o dal browser stesso.
- PWABuilder:il progetto principale utilizzato da Goodnotes per eseguire il wrapping della PWA in un file binario di Windows nativo in modo che il team possa distribuire la nostra app dal Microsoft Store.
- Attività web attendibili:la tecnologia Android più importante utilizzata dalla società per distribuire la nostra PWA come applicazione nativa.
La figura seguente mostra cosa viene implementato utilizzando TypeScript e React classici e cosa viene implementato utilizzando SwiftWasm e JavaScript, Swift e WebAssembly. Questa parte del progetto utilizza JSKit, una libreria di interoperabilità JavaScript per Swift e WebAssembly che il team utilizza per gestire il DOM nella schermata dell'editor dal codice Swift, se necessario, o persino per utilizzare alcune API specifiche del browser.
Perché utilizzare Wasm e il web?
Anche se Wasm non è supportato ufficialmente da Apple, i seguenti motivi sono la ragione per cui il team di ingegneria di Goodnotes ha ritenuto che questo approccio fosse la decisione migliore:
- Il riutilizzo di più di 100.000 righe di codice.
- La possibilità di continuare lo sviluppo del prodotto principale e al contempo contribuire alle app multipiattaforma.
- La possibilità di raggiungere tutte le piattaforme il prima possibile utilizzando un processo di sviluppo iterativo.
- Avere il controllo per eseguire il rendering dello stesso documento senza duplicare tutta la logica di business e introdurre differenze nelle nostre implementazioni.
- Usufruire contemporaneamente di tutti i miglioramenti delle prestazioni apportati su ogni piattaforma (e di tutte le correzioni di bug implementate su ogni piattaforma).
Il riutilizzo di oltre 100 mila righe di codice e della logica di business che implementa la nostra pipeline di rendering è stato fondamentale. Allo stesso tempo, la compatibilità del codice Swift con altre toolchain consente di riutilizzarlo in piattaforme diverse in futuro, se necessario.
Sviluppo iterativo del prodotto
Il team ha adottato un approccio iterativo per offrire agli utenti un prodotto il più rapidamente possibile. Goodnotes è nato con una versione di sola lettura del prodotto in cui gli utenti potevano recuperare qualsiasi documento condiviso e leggerlo da qualsiasi piattaforma. Con un semplice link, potrà accedere e leggere le stesse note che ha scritto sul suo iPad. La fase successiva ha aggiunto funzionalità di modifica per rendere le versioni multipiattaforma equivalenti a quella per iOS.
Lo sviluppo della prima versione del prodotto di sola lettura ha richiesto sei mesi, mentre i nove mesi successivi sono stati dedicati al primo gruppo di funzionalità di modifica e alla schermata dell'interfaccia utente in cui puoi controllare tutti i documenti che hai creato o che qualcuno ha condiviso con te. Inoltre, le nuove funzionalità della piattaforma iOS sono state facili da trasferire al progetto multipiattaforma grazie alla suite di strumenti SwiftWasm. Ad esempio, è stato creato un nuovo tipo di penna e implementato facilmente su più piattaforme riutilizzando migliaia di righe di codice.
La realizzazione di questo progetto è stata un'esperienza incredibile e Goodnotes ha imparato molto. Ecco perché le sezioni seguenti si concentreranno su aspetti tecnici interessanti dello sviluppo web e sull'utilizzo di WebAssembly e linguaggi come Swift.
Ostacoli iniziali
Lavorare a questo progetto è stato molto impegnativo da molti punti di vista. Il primo ostacolo rilevato dal team riguardava la toolchain SwiftWasm. La toolchain è stata di grande aiuto per il team, ma non tutto il codice iOS era compatibile con Wasm. Ad esempio, il codice relativo all'I/O o all'interfaccia utente, come l'implementazione di visualizzazioni, client API o l'accesso al database, non era riutilizzabile, quindi il team doveva iniziare a eseguire il refactoring di parti specifiche dell'app per poterle riutilizzare dalla soluzione multipiattaforma. La maggior parte delle PR create dal team riguardava il refactoring per astrarre le dipendenze, in modo che il team potesse sostituirle in un secondo momento utilizzando l'iniezione di dipendenze o altre strategie simili. Il codice iOS inizialmente combinava la logica di business non elaborata che poteva essere implementata in Wasm con il codice responsabile dell'input/output e dell'interfaccia utente che non poteva essere implementato in Wasm perché non supportato. Pertanto, il codice di I/O e UI doveva essere nuovamente implementato in TypeScript una volta che la logica di business di Swift era pronta per essere riutilizzata tra le piattaforme.
Problemi di prestazioni risolti
Una volta che Goodnotes ha iniziato a lavorare sull'editor, il team ha identificato alcuni problemi con l'esperienza di modifica e vincoli tecnologici impegnativi sono stati inseriti nella nostra roadmap. Il primo problema riguardava il rendimento. JavaScript è un linguaggio a thread singolo. Ciò significa che ha uno stack di chiamate e un heap di memoria. Esegue il codice in ordine e deve completare l'esecuzione di un frammento di codice prima di passare al successivo. È sincrono, ma a volte può essere dannoso. Ad esempio, se un'esecuzione di una funzione richiede un po' di tempo o deve attendere qualcosa, nel frattempo tutto viene bloccato. Ed è esattamente quello che gli ingegneri dovevano risolvere. La valutazione di alcuni percorsi specifici nella nostra base di codice relativi al livello di rendering o ad altri algoritmi complessi era un problema per il team, perché questi algoritmi erano sincroni e la loro esecuzione bloccava il thread principale. Il team di GoodNotes le ha riscritte per renderle più veloci e ne ha ristrutturate alcune per renderle asincrone. È stata inoltre introdotta una strategia di rendimento in modo che l'app possa interrompere l'esecuzione dell'algoritmo e riprenderla in un secondo momento, consentendo al browser di aggiornare l'interfaccia utente ed evitare la perdita di frame. Questo non ha rappresentato un problema per l'applicazione iOS, in quanto può utilizzare thread e valutare questi algoritmi in background mentre il thread principale di iOS aggiorna l'interfaccia utente.
Un'altra soluzione che il team di ingegneri ha dovuto risolvere è stata la migrazione di un'interfaccia utente basata su elementi HTML associati al DOM a un'interfaccia utente del documento basata su un canvas a schermo intero. Il progetto ha iniziato a mostrare tutte le note e i contenuti relativi a un documento all'interno della struttura DOM utilizzando elementi HTML come farebbe qualsiasi altra pagina web, ma a un certo punto è stata eseguita la migrazione a una tela a schermo intero per migliorare le prestazioni sui dispositivi di fascia bassa riducendo il tempo di lavoro del browser su gli aggiornamenti DOM.
Il team di ingegneria ha identificato le seguenti modifiche che potrebbero aver ridotto alcuni dei problemi riscontrati, se fossero state apportate all'inizio del progetto.
- Scarica di più il thread principale utilizzando spesso i worker web per algoritmi pesanti.
- Utilizza le funzioni esportate e le funzioni importate anziché la libreria di interoperabilità JS-Swift fin dall'inizio per ridurre l'impatto sulle prestazioni dell'uscita dal contesto Wasm. Questa libreria di interoperabilità JavaScript è utile per accedere al DOM o al browser, ma è più lenta delle funzioni esportate da Wasm native.
- Assicurati che il codice consenta l'utilizzo di
OffscreenCanvas
sotto il cofano in modo che l'app possa scaricare il thread principale e spostare tutto l'utilizzo di l'API Canvas in un web worker per massimizzare il rendimento delle applicazioni quando si scrivono le note. - Sposta tutta l'esecuzione relativa a Wasm in un worker web o addirittura in un pool di worker web in modo che l'app possa ridurre il carico di lavoro del thread principale.
L'editor di testo
Un altro problema interessante riguardava uno strumento specifico, l'editor di testo.
L'implementazione di questo strumento per iOS si basa su
NSAttributedString
,
un piccolo set di strumenti che utilizza
RTF
sotto il cofano. Tuttavia, questa implementazione non è compatibile con SwiftWasm, pertanto il team multipiattaforma è stato costretto a creare innanzitutto un parser personalizzato basato sulla grammatica RTF e in un secondo momento a implementare l'esperienza di modifica trasformando il formato RTF in HTML e viceversa. Nel frattempo, il team di iOS ha iniziato a lavorare alla nuova implementazione
per questo strumento sostituendo l'utilizzo di RTF con un modello personalizzato in modo che l'app possa rappresentare il testo stilizzato in modo semplice per tutte le piattaforme che condividono lo stesso codice Swift.
Questa sfida è stata uno dei punti più interessanti della roadmap del progetto perché è stata risolta in modo iterativo in base alle esigenze dell'utente. Si trattava di un problema di ingegneria risolto utilizzando un approccio incentrato sull'utente in cui il team doveva riscrivere parte del codice per poter visualizzare il testo, quindi ha attivato la modifica del testo in una seconda release.
Release iterative
L'evoluzione del progetto negli ultimi due anni è stata incredibile. Il team ha iniziato a lavorare a una versione di sola lettura del progetto e mesi dopo ha rilasciato una versione completamente nuova con molte funzionalità di modifica. Per rilasciare frequentemente le modifiche al codice in produzione, il team ha deciso di utilizzare ampiamente i flag delle funzionalità. Per ogni release, il team potrebbe attivare nuove funzionalità e rilasciare anche modifiche al codice che implementano nuove funzionalità che l'utente vedrà settimane dopo. Tuttavia, il team ritiene di poter migliorare qualcosa. Ritengono che l'introduzione di un sistema di flag delle funzionalità dinamici avrebbe contribuito ad accelerare le operazioni, in quanto eliminerebbe la necessità di un nuovo deployment per modificare i valori dei flag. In questo modo, Goodnotes avrebbe una maggiore flessibilità e l'implementazione della nuova funzionalità verrebbe accelerata perché non sarebbe necessario collegare l'implementazione del progetto al rilascio del prodotto.
Lavoro offline
Una delle funzionalità principali su cui ha lavorato il team è il supporto offline. La possibilità di modificare i documenti è una funzionalità che ti aspetteresti da qualsiasi applicazione come questa. Tuttavia, non si tratta di una funzionalità semplice perché Goodnotes supporta la collaborazione. Ciò significa che tutte le modifiche apportate da utenti diversi su dispositivi diversi devono essere applicate a ogni dispositivo senza che gli utenti debbano risolvere eventuali conflitti. Goodnotes ha risolto questo problema molto tempo fa utilizzando CRDT sotto il cofano. Grazie a questi tipi di dati replicati senza conflitti, Goodnotes è in grado di combinare tutte le modifiche apportate a qualsiasi documento da qualsiasi utente e unire le modifiche senza conflitti di unione. L'utilizzo di IndexedDB e dello spazio di archiviazione disponibile per i browser web è stato un fattore determinante per l'esperienza offline collaborativa sul web.
Inoltre, l'apertura dell'app web Goodnotes comporta un costo iniziale di download di circa 40 MB a causa delle dimensioni del file binario Wasm. Inizialmente, il team di Goodnotes si basava esclusivamente sulla cache del browser normale per il bundle dell'app e per la maggior parte degli endpoint API utilizzati, ma a posteriori avrebbe potuto trarre vantaggio dall'API Cache e dai service worker più affidabili. Inizialmente il team si è tirato indietro a causa della presunta complessità dell'attività, ma alla fine ha capito che Workbox la rendeva molto meno spaventosa.
Consigli per l'utilizzo di Swift sul web
Se hai un'applicazione per iOS con molto codice che vuoi riutilizzare, preparati perché stai per iniziare un viaggio incredibile. Ecco alcuni suggerimenti che potresti trovare interessanti prima di iniziare.
- Controlla il codice che vuoi riutilizzare. Se la logica di business della tua app è implementata sul lato server, è probabile che tu voglia riutilizzare il codice dell'interfaccia utente e Wasm non ti sarà di aiuto. Il team ha esaminato brevemente Tokamak, un framework compatibile con SwiftUI per la creazione di app browser con WebAssembly, ma non era sufficientemente maturo per le esigenze dell'app. Tuttavia, se la tua app ha una logica aziendale o algoritmi efficaci implementati nell'ambito del codice client, Wasm sarà il tuo migliore amico.
- Assicurati che la base di codice Swift sia pronta. I pattern di progettazione del software per il livello UI o architetture specifiche che creano una forte separazione tra la logica dell'interfaccia utente e la logica di business saranno molto utili perché non potrai riutilizzare l'implementazione del livello UI. Anche l'architettura pulita o i principi dell'architettura esagonale saranno fondamentali, perché dovrai iniettare e fornire le dipendenze per tutto il codice relativo all'I/O e sarà molto più facile farlo se segui queste architetture in cui i dettagli di implementazione sono definiti come astrazione e il principio di inversione delle dipendenze viene ampiamente utilizzato.
- Wasm non fornisce codice UI. Pertanto, scegli il framework UI che vuoi utilizzare per il web.
- JSKit ti aiuterà a integrare il codice Swift con JavaScript, ma tieni presente che se hai un hotpath, il passaggio del ponte JS-Swift potrebbe essere costoso e dovrai sostituirlo con le funzioni esportate. Per scoprire di più sul funzionamento di JSKit, consulta la documentazione ufficiale e l'articolo Dynamic Member Lookup in Swift, a hidden gem!.
- La possibilità di riutilizzare l'architettura dipende dall'architettura dell'app e dalla libreria del meccanismo di esecuzione del codice asincrono che utilizzi. Pattern come MVVP o l'architettura componibile ti aiuteranno a riutilizzare i modelli di visualizzazione e parte della logica dell'interfaccia utente senza accoppiare l'implementazione alle dipendenze di UIKit che non puoi utilizzare con Wasm. RXSwift e altre librerie potrebbero non essere compatibili con Wasm, quindi tieni presente che dovrai utilizzare OpenCombine, async/await e gli stream nel codice Swift di Goodnotes.
- Comprimi il file binario Wasm utilizzando gzip o brotli. Tieni presente che le dimensioni del file binario saranno piuttosto grandi per le applicazioni web classiche.
- Anche se puoi utilizzare Wasm senza la PWA, assicurati di includere almeno un servizio worker, anche se la tua app web non ha un file manifest o non vuoi che l'utente lo installi. Il service worker salverà e pubblicherà senza costi il file binario Wasm e tutte le risorse dell'app, in modo che l'utente non debba scaricarli ogni volta che apre il progetto.
- Tieni presente che l'assunzione potrebbe essere più difficile del previsto. Potresti dover assumere sviluppatori web esperti con una certa esperienza in Swift o sviluppatori Swift esperti con una certa esperienza sul web. Sarebbe fantastico se riuscissi a trovare ingegneri generici con alcune conoscenze su entrambe le piattaforme
Conclusioni
Creare un progetto web utilizzando una complessa tecnologia e lavorare a un prodotto pieno di sfide è un'esperienza incredibile. Sarà difficile, ma ne varrà la pena. Goodnotes non avrebbe mai potuto rilasciare una versione per Windows, Android, ChromeOS e il web mentre lavorava a nuove funzionalità per l'applicazione per iOS senza utilizzare questo approccio. Grazie a questo stack tecnologico e al team di ingegneri di Goodnotes, ora Goodnotes è ovunque e il team è pronto a continuare a lavorare per affrontare le prossime sfide. Se vuoi saperne di più su questo progetto, puoi guardare un intervento del team di Goodnotes tenuto al NSSpain 2023. Assicurati di provare Goodnotes per il web.