Negli ultimi due anni, il team tecnico di Goodnotes ha lavorato a un progetto per portare l'efficace app per prendere appunti per iPad su altre piattaforme. Questo case study spiega come l'app per iPad dell'anno 2022 è arrivata su Web, ChromeOS, Android e Windows con tecnologie web e WebAssembly riutilizzando lo stesso codice Swift su cui il team lavora da più di dieci anni.
Perché Goodnotes è apparso sul web, su Android e Windows
Nel 2021 Goodnotes era disponibile solo come app per iOS e iPad. Il team di tecnici di Goodnotes ha accettato un'enorme sfida tecnica: ha creato una nuova versione di Goodnotes, ma utile per altri sistemi operativi e piattaforme. Il prodotto deve essere completamente compatibile con le stesse note dell'applicazione iOS e il relativo rendering. Qualsiasi nota presa sopra un PDF o qualsiasi immagine allegata deve essere equivalente e mostrare gli stessi tratti mostrati nell'app per iOS. Ogni tratto aggiunto deve essere equivalente a quello creato dagli utenti iOS, indipendentemente dallo strumento utilizzato dall'utente, ad esempio penna, evidenziatore, penna stilografica, forme o gomma.
Sulla base dei requisiti e dell'esperienza del team di tecnici, il team ha concluso rapidamente che il riutilizzo del codebase Swift sarebbe stato la migliore linea d'azione, dato che era già stato scritto e testato nel corso di molti anni. Ma perché non trasferire semplicemente l'applicazione per iOS/iPad già esistente su un'altra piattaforma o tecnologia come Flutter o Compose Multiplatform? Il passaggio a una nuova piattaforma implicherebbe la riscrittura di Goodnotes. Questa operazione potrebbe avviare una corsa di sviluppo tra l'applicazione iOS già implementata e un'applicazione da creare da zero nuove applicazioni o comportare l'interruzione di un nuovo sviluppo nell'applicazione esistente mentre il nuovo codebase raggiungeva i risultati. Se Goodnotes potesse riutilizzare il codice Swift, il team potrebbe trarre vantaggio dalle nuove funzionalità implementate dal team iOS, mentre il team multipiattaforma lavorava alle nozioni di base delle app e a raggiungere la parità delle funzionalità.
Il prodotto aveva già risolto una serie di sfide interessanti per iOS per l'aggiunta di funzionalità quali:
- Rendering 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 del modello di IA.
- Ricerca di contenuti e indicizzazione dei documenti.
- Esperienza di scorrimento e animazioni personalizzate.
- Visualizza l'implementazione del modello per tutti i livelli UI.
Tutti questi strumenti sarebbero molto più facili da implementare per altre piattaforme se il team di progettazione fosse riuscito a far funzionare il codebase iOS già per le applicazioni iOS e iPad ed eseguirlo come parte di un progetto che Goodnotes potrebbe essere fornito come applicazioni web, Windows e Android.
Stack tecnico 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 programma binario potrebbe essere incluso in una pagina web fornita come applicazione web progressiva per Android, Windows, ChromeOS e qualsiasi altro sistema operativo.
Lo scopo era rilasciare Goodnotes come PWA e mostrarlo nello store di ogni piattaforma. Oltre a Swift, il linguaggio di programmazione già utilizzato per iOS e a WebAssembly 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 bundler più popolari per il web.
- PWA e service worker: enabler enormi per questo progetto perché il team potrebbe fornire la nostra app come applicazione offline che funziona come qualsiasi altra app per iOS e puoi installarla dallo store o dal browser stesso.
- PWABuilder: il progetto principale utilizzato da Goodnotes per aggregare la PWA in un file binario nativo di Windows in modo che il team possa distribuire la nostra app dal Microsoft Store.
- Attività web attendibili:la tecnologia Android più importante utilizzata dall'azienda per distribuire la nostra PWA come applicazione nativa in background.
La figura seguente mostra cosa viene implementato utilizzando le versioni classiche di TypeScript e React e cosa viene implementato utilizzando SwiftWasm e vanilla JavaScript, Swift e WebAssembly. Questa parte del progetto utilizza JSKit, una libreria di interoperabilità JavaScript per Swift e WebAssembly utilizzata dal team per gestire il DOM nella schermata di editor dal nostro codice Swift quando necessario o anche utilizzare alcune API specifiche del browser.
Perché utilizzare Wasm e il web?
Anche se Wasm non è ufficialmente supportato da Apple, i seguenti motivi sono i motivi per cui il team di tecnici di Goodnotes ha ritenuto che questo approccio fosse la decisione migliore:
- Riutilizzo di oltre 100.000 righe di codice.
- La possibilità di continuare a sviluppare il prodotto principale, contribuendo al contempo alle app multipiattaforma.
- L'opportunità di accedere a ogni piattaforma il prima possibile grazie a un processo di sviluppo iterativo.
- Avere il controllo di visualizzare lo stesso documento senza duplicare tutta la logica di business e introdurre differenze nelle nostre implementazioni.
- Usufruisci di tutti i miglioramenti delle prestazioni apportati su ogni piattaforma contemporaneamente (e di tutte le correzioni di bug implementate su ogni piattaforma).
Il riutilizzo di oltre 100.000 righe di codice e della logica di business che implementava la nostra pipeline di rendering è stato fondamentale. Allo stesso tempo, rendere il codice Swift compatibile con altre toolchain consente di riutilizzare in futuro questo codice in diverse piattaforme, se necessario.
Sviluppo iterativo dei prodotti
Il team ha adottato un approccio iterativo per offrire qualcosa agli utenti il più rapidamente possibile. Goodnotes ha iniziato con una versione di sola lettura del prodotto in cui gli utenti potevano ottenere qualsiasi documento condiviso e leggerlo da qualsiasi piattaforma. Basta un link per accedere e leggere le stesse note che hanno scritto dai propri iPad. Aggiunta la fase successiva alla modifica delle funzionalità, per rendere le versioni multipiattaforma equivalenti a quella per iOS.
Lo sviluppo della prima versione del prodotto di sola lettura ha richiesto sei mesi, i nove mesi successivi sono stati dedicati alla prima serie di funzionalità di modifica e alla schermata dell'interfaccia utente in cui è possibile controllare tutti i documenti creati o che qualcuno ha condiviso con te. Inoltre, è stato facile trasferire le nuove funzionalità della piattaforma iOS al progetto multipiattaforma grazie alla SwiftWasm Toolchain. Ad esempio, è stato creato un nuovo tipo di penna, che è stato facilmente implementato multipiattaforma riutilizzando migliaia di righe di codice.
La realizzazione di questo progetto è stata un'esperienza incredibile e Goodnotes ne ha imparato molto. Per questo motivo, le sezioni seguenti si concentreranno su punti tecnici interessanti relativi allo sviluppo web, all'utilizzo di WebAssembly e a linguaggi come Swift.
Ostacoli iniziali
Lavorare a questo progetto è stato molto difficile da tanti punti di vista diversi. Il primo ostacolo trovato dal team era legato alla toolchain di SwiftWasm. La toolchain è stata un fattore determinante per il team, ma non tutto il codice iOS era compatibile con Wasm. Ad esempio, il codice relativo a IO o UI, come l'implementazione di viste, client API o l'accesso al database, non era riutilizzabile, quindi il team ha dovuto iniziare il refactoring di parti specifiche dell'app per poterle riutilizzare dalla soluzione multipiattaforma. La maggior parte delle PR create dal team consisteva nel refactoring per astrarre le dipendenze in modo che in seguito potessero sostituirle utilizzando l'inserimento delle dipendenze o altre strategie simili. Il codice iOS originariamente combinava una logica di business non elaborata che potrebbe essere implementata in Wasm con un codice responsabile di input/output e dell'interfaccia utente che non poteva essere implementato in Wasm perché non supportava nemmeno Wasm. Di conseguenza, quando la logica di business di Swift era pronta per essere riutilizzata tra le piattaforme, era necessario implementare nuovamente IO e codice UI in TypeScript.
Problemi di rendimento risolti
Quando Goodnotes ha iniziato a lavorare sull'editor, il team ha identificato alcuni problemi con l'esperienza di editing e la nostra roadmap presenta problemi tecnologici complessi. 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 terminare l'esecuzione di una porzione di codice prima di passare alla successiva. È sincrono, ma a volte può essere dannoso. Ad esempio, se l'esecuzione di una funzione richiede un po' di tempo o deve attendere qualcosa, nel frattempo blocca tutto. Ed è esattamente quello che gli ingegneri hanno dovuto risolvere. La valutazione di alcuni percorsi specifici nel nostro codebase relativi al livello di rendering o ad altri algoritmi complessi era un problema per il team, poiché questi algoritmi erano sincroni e la loro esecuzione bloccava il thread principale. Il team di Goodnotes li ha riscritti per renderli più veloci e ne ha refactoring alcuni per renderli asincroni. Ha anche introdotto una strategia di rendimento in modo che l'app possa interrompere l'esecuzione dell'algoritmo e continuarla in un secondo momento, consentendo al browser di aggiornare l'interfaccia utente ed evitare di perdere frame. Questo non era un problema per l'applicazione iOS, perché può utilizzare i thread e valutare questi algoritmi in background mentre il thread principale di iOS aggiorna l'interfaccia utente.
Un'altra soluzione che il team di tecnici ha dovuto risolvere era la migrazione di un'interfaccia utente basata su elementi HTML collegati al DOM a un'interfaccia utente di documenti basata su canvas a schermo intero. Il progetto ha iniziato a mostrare tutte le note e i contenuti relativi a un documento come parte della struttura DOM utilizzando elementi HTML come farebbe qualsiasi altra pagina web, ma a un certo punto è stata eseguita la migrazione a un canvas a schermo intero per migliorare le prestazioni sui dispositivi di fascia bassa riducendo il tempo di lavoro degli aggiornamenti DOM da parte del browser.
Il team tecnico ha identificato le seguenti modifiche come elementi che avrebbero potuto ridurre alcuni dei problemi riscontrati, se l'avesse eseguita all'inizio del progetto.
- Scaricare ulteriormente il thread principale utilizzando spesso i web worker per gli algoritmi complessi.
- Utilizza le funzioni esportate e importate invece della libreria di interoperabilità JS-Swift sin dall'inizio in modo che possa ridurre l'impatto sulle prestazioni dell'uscita dal contesto Wasm. Questa libreria di interoperabilità JavaScript è utile per ottenere l'accesso al DOM o al browser, ma è più lenta rispetto alle funzioni native esportate in Wasm.
- Assicurati che il codice consenta l'utilizzo di
OffscreenCanvas
in background, in modo che l'app possa trasferire il thread principale e spostare tutti gli utilizzi dell'API Canvas su un web worker, massimizzando le prestazioni delle applicazioni durante la scrittura delle note. - Sposta tutte le esecuzioni correlate a Wasm a un web worker o anche a 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 in iOS di questo strumento si basa su NSAttributedString
, un piccolo set di strumenti che utilizza RTF in background. Tuttavia, questa implementazione non è compatibile con SwiftWasm, quindi
il team multipiattaforma è stato costretto prima a creare
un parser personalizzato basato sulla grammatica RTF
e in seguito a implementare l'esperienza di modifica trasformando RTF in HTML e
viceversa. Nel frattempo, il team iOS ha iniziato a lavorare alla nuova implementazione di questo strumento, sostituendo l'utilizzo di RTF con un modello personalizzato, in modo che l'app possa rappresentare il testo con stili in modo intuitivo 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 iterativamente 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 eseguire il rendering del testo in modo da abilitare l'editing del testo in una seconda versione.
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 distribuito una nuova versione con molte funzionalità di modifica. Per rilasciare di frequente modifiche al codice in produzione, il team ha deciso di utilizzare ampiamente i flag delle funzionalità. Per ogni release, il team ha potuto abilitare nuove funzionalità e anche rilasciare modifiche al codice che implementano nuove funzionalità che l'utente vedeva settimane dopo. Tuttavia, c'è qualcosa che il team pensa che avrebbe potuto migliorare. Pensano che l'introduzione di un sistema di flag delle funzionalità dinamici avrebbe aiutato a velocizzare le cose, in quanto ridurrebbe la necessità di un nuovo deployment per modificare i valori dei flag. Questo offrirebbe una maggiore flessibilità e velocizzerà il deployment della nuova funzionalità, in quanto Goodnotes non dovrà collegare il deployment del progetto alla release del prodotto.
Lavoro offline
Una delle funzionalità principali su cui il team ha lavorato è l'assistenza offline. La possibilità di modificare i documenti è una funzionalità che ci si aspetta da qualsiasi applicazione come questa. Tuttavia, non è una funzionalità semplice, perché Goodnotes supporta la collaborazione. Ciò significa che tutte le modifiche apportate da utenti diversi sui diversi dispositivi dovrebbero essere applicate a ogni dispositivo senza chiedere agli utenti di risolvere eventuali conflitti. I Goodnote hanno risolto questo problema molto tempo fa grazie all'utilizzo di CRDT in background. Grazie a questi tipi di dati replicati senza conflitti, Goodnotes è in grado di combinare tutte le modifiche apportate da qualsiasi utente a qualsiasi documento e di unire le modifiche senza creare conflitti di unione. L'utilizzo di IndexedDB e lo spazio di archiviazione disponibile per i browser web ha rappresentato un enorme potenziale per l'esperienza offline collaborativa sul web.
Inoltre, l'apertura dell'app web Goodnotes comporta un costo iniziale di download iniziale di circa 40 MB a causa delle dimensioni binarie di Wasm. Inizialmente, il team di Goodnotes si è affidato esclusivamente alla normale cache del browser per l'app bundle in sé e per la maggior parte degli endpoint API che utilizza, ma con gli occhi del futuro avrebbe potuto trarre profitto dall'API Cache e dai service worker più affidabili. Inizialmente il team si era allontanato da questa attività a causa della sua presunta complessità, ma alla fine si è reso conto che Workbox la rendeva molto meno inquietante.
Consigli per l'utilizzo di Swift sul web
Se hai un'applicazione iOS con molto codice che vuoi riutilizzare, preparati perché stai per iniziare un viaggio incredibile. Ci sono alcuni suggerimenti che potrebbero interessarti prima di iniziare.
- Seleziona il codice che vuoi riutilizzare. Se la logica di business della tua app è implementata sul lato server, è probabile che tu voglia riutilizzare il codice UI e Wasm non ti aiuterà in questo caso. Il team ha esaminato brevemente Tokamak, un framework compatibile con SwiftUI per la creazione di app del browser con WebAssembly, ma non era sufficientemente maturo per le esigenze dell'app. Tuttavia, se l'app ha una logica di business o algoritmi implementati nel codice client solidi, Wasm sarà il tuo migliore amico.
- Assicurati che il codebase Swift sia pronto. I pattern di progettazione software per il livello UI o per architetture specifiche che creano una forte separazione tra la logica dell'interfaccia utente e la logica di business saranno davvero utili, perché non potrai riutilizzare l'implementazione del livello UI. Anche l'architettura pulita o i principi dell'architettura esagonale saranno fondamentali, perché dovrai inserire e fornire dipendenze per tutto il codice relativo all'IO e sarà molto più facile se segui queste architetture in cui i dettagli di implementazione sono definiti come astrazioni e il principio di inversione delle dipendenze è molto utilizzato.
- Wasm non fornisce codice UI. Decidi quindi il framework UI che vuoi usare per il web.
- JSKit ti aiuterà a integrare il codice Swift con JavaScript, ma tieni presente che, se disponi di un hotpath, l'attraversamento del bridge JS-Swift potrebbe essere costoso e dovresti sostituirlo con funzioni esportate. Scopri di più sul funzionamento di JSKit nella documentazione ufficiale e nel post Dynamic Member Lookup in Swift, una perla nascosta!.
- La possibilità di riutilizzare l'architettura dipende dall'architettura seguita dall'app e dalla libreria dei meccanismi di esecuzione del codice asincrono che utilizzi. Pattern come MVVP o architettura componibile ti aiuteranno a riutilizzare i modelli di visualizzazione e parte della logica dell'interfaccia utente senza associare l'implementazione a dipendenze UIKit che non puoi utilizzare con Wasm. RXSwift e altre librerie potrebbero non essere compatibili con Wasm, quindi tienilo presente perché 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 service worker, anche se la tua app web non ha manifest o se non vuoi che l'utente la installi. Il service worker salverà e gestirà senza costi il programma 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 assumere persone potrebbe essere più difficile del previsto. Potrebbe essere necessario assumere sviluppatori web esperti con una certa esperienza su Swift o sviluppatori Swift solidi con una certa esperienza sul web. Se riuscissi a trovare ingegneri generalisti con una certa conoscenza di entrambe le piattaforme, sarebbe fantastico
Conclusioni
Creare un progetto web utilizzando uno stack tecnico complesso mentre lavori a un prodotto pieno di sfide è un'esperienza incredibile. Sarà difficile, ma ne vale assolutamente la pena. Senza utilizzare questo approccio, Goodnotes non avrebbe mai potuto rilasciare una versione per Windows, Android, ChromeOS e il web mentre lavorava alle nuove funzionalità per l'applicazione iOS. Grazie a questo stack tecnico e al team di progettazione di Goodnotes, ora è ovunque e il team è pronto a continuare a lavorare alle prossime sfide. Per saperne di più su questo progetto, puoi guardare una conferenza tenuta dal team Goodnotes a NSSpagna 2023. Assicurati di provare Goodnotes for web!