Überall Goodnotes

Marketingbild von Goodnotes, auf dem eine Frau das Produkt auf einem iPad verwendet

In den letzten zwei Jahren hat das Entwicklerteam von Goodnotes an einem Projekt gearbeitet, um die erfolgreiche iPad-Notiz-App auf andere Plattformen zu bringen. In dieser Fallstudie erfahren Sie, wie die iPad-App des Jahres 2022 mithilfe von Webtechnologien und WebAssembly für das Web, ChromeOS, Android und Windows portiert wurde. Dabei wurde derselbe Swift-Code verwendet, an dem das Team seit mehr als zehn Jahren arbeitet.

Goodnotes-Logo

Warum GoodNotes jetzt auch im Web, für Android und Windows verfügbar ist

2021 war Goodnotes nur als App für iOS und iPad verfügbar. Das Entwicklungsteam von Goodnotes hat sich einer großen technischen Herausforderung gestellt: Eine neue Version von Goodnotes für zusätzliche Betriebssysteme und Plattformen zu entwickeln. Das Produkt muss vollständig mit der iOS-App kompatibel sein und dieselben Notizen anzeigen. Alle Notizen, die auf einer PDF-Datei erstellt wurden, oder alle angehängten Bilder sollten dem Original entsprechen und dieselben Striche enthalten, die in der iOS-App zu sehen sind. Jeder hinzugefügte Strich sollte dem entsprechen, den iOS-Nutzer erstellen können, unabhängig vom verwendeten Tool – z. B. Stift, Textmarker, Füllfeder, Formen oder Radiergummi.

Vorschau der GoodNotes App mit handschriftlichen Notizen und Skizzen.

Auf Grundlage der Anforderungen und der Erfahrung des Entwicklerteams kam das Team schnell zu dem Schluss, dass die Wiederverwendung der Swift-Codebasis die beste Vorgehensweise wäre, da sie bereits geschrieben und über viele Jahre hinweg gut getestet wurde. Aber warum nicht einfach die bereits vorhandene iOS-/iPad-Anwendung auf eine andere Plattform oder Technologie wie Flutter oder Compose Multiplatform portieren? Ein Wechsel zu einer neuen Plattform würde eine Neuprogrammierung von Goodnotes erfordern. Dies könnte zu einem Entwicklungsrennen zwischen der bereits implementierten iOS-Anwendung und einer neuen Anwendung führen, die von Grund auf neu erstellt werden muss, oder dazu, dass die Weiterentwicklung der vorhandenen Anwendung unterbrochen wird, während die neue Codebasis aufgeholt wird. Wenn Goodnotes den Swift-Code wiederverwenden könnte, könnte das Team von den neuen Funktionen profitieren, die vom iOS-Team implementiert wurden, während das plattformübergreifende Team an den Grundlagen der App arbeitete, und so die Funktionsparität erreichen.

Das Produkt hatte bereits eine Reihe interessanter Herausforderungen für iOS gelöst, um Funktionen wie die folgenden hinzuzufügen:

  • Darstellung von Notizen
  • Synchronisierung von Dokumenten und Notizen
  • Konfliktlösung für Notizen mithilfe von konfliktfreien replizierten Datentypen.
  • Datenanalyse für die Bewertung von KI-Modellen.
  • Inhaltssuche und Dokumentindexierung
  • Benutzerdefiniertes Scrollen und Animationen
  • Modellimplementierung für alle UI-Ebenen ansehen

Alle diese Funktionen wären für andere Plattformen viel einfacher zu implementieren, wenn das Engineering-Team die iOS-Codebasis, die bereits für iOS- und iPad-Anwendungen funktioniert, als Teil eines Projekts ausführen könnte, das Goodnotes als Windows-, Android- oder Webanwendung bereitstellen könnte.

Der Technologie-Stack von Goodnotes

Glücklicherweise gab es eine Möglichkeit, den vorhandenen Swift-Code im Web wiederzuverwenden: WebAssembly (Wasm). Goodnotes hat einen Prototyp mit Wasm und dem Open-Source- und Community-gepflegten Projekt SwiftWasm erstellt. Mit SwiftWasm konnte das Goodnotes-Team eine Wasm-Binärdatei mit dem bereits implementierten Swift-Code generieren. Dieses Binärprogramm kann in eine Webseite eingefügt werden, die als progressive Webanwendung für Android, Windows, ChromeOS und jedes andere Betriebssystem bereitgestellt wird.

Die Einführung von Goodnotes beginnt mit Chrome, gefolgt von Windows, Android und anderen Plattformen wie Linux. Alle basieren auf der PWA.

Ziel war es, Goodnotes als PWA zu veröffentlichen und im Store jeder Plattform anbieten zu können. Neben Swift, der bereits für iOS verwendeten Programmiersprache, und WebAssembly, mit dem Swift-Code im Web ausgeführt wird, wurden für das Projekt die folgenden Technologien verwendet:

  • TypeScript: Die am häufigsten verwendete Programmiersprache für Webtechnologien.
  • React und Webpack:Das beliebteste Framework und Bundler für das Web.
  • PWA und Dienst-Worker: Diese Technologien waren für dieses Projekt von entscheidender Bedeutung, da das Team unsere App als Offline-Anwendung veröffentlichen konnte, die wie jede andere iOS-App funktioniert und über den Store oder den Browser selbst installiert werden kann.
  • PWABuilder: Das Hauptprojekt, mit dem Goodnotes die PWA in ein natives Windows-Binärformat verpackt, damit das Team unsere App über den Microsoft Store vertreiben kann.
  • Vertrauenswürdige Web-Aktivitäten:Die wichtigste Android-Technologie, mit der das Unternehmen unsere PWA als native Anwendung bereitstellt.

Der Goodnotes-Technologie-Stack besteht aus Swift, Wasm, React und PWA.

Die folgende Abbildung zeigt, was mit klassischem TypeScript und React implementiert wird und was mit SwiftWasm und Vanilla-JavaScript, Swift und WebAssembly. In diesem Teil des Projekts wird JSKit verwendet, eine JavaScript-Interoperabilitätsbibliothek für Swift und WebAssembly, mit der das Team bei Bedarf das DOM auf dem Editorbildschirm über unseren Swift-Code verwalten oder sogar einige browserspezifische APIs verwenden kann.

App-Screenshots auf Mobilgeräten und Computern, die die einzelnen Zeichenbereiche zeigen, die von Wasm gesteuert werden, und die UI-Bereiche, die von React gesteuert werden.

Warum Wasm und das Web?

Auch wenn Wasm von Apple nicht offiziell unterstützt wird, hat das GoodNotes-Entwicklerteam aus folgenden Gründen beschlossen, diesen Ansatz zu verfolgen:

  • Die Wiederverwendung von mehr als 100.000 Codezeilen.
  • Die Möglichkeit, die Entwicklung des Hauptprodukts fortzusetzen und gleichzeitig an den plattformübergreifenden Apps mitzuwirken.
  • Mit einem iterativen Entwicklungsvorgang können Sie so schnell wie möglich auf allen Plattformen vertreten sein.
  • Wir wollten die Möglichkeit haben, dasselbe Dokument zu rendern, ohne die gesamte Geschäftslogik zu duplizieren und Unterschiede in unseren Implementierungen vorzunehmen.
  • Sie profitieren von allen Leistungsverbesserungen, die gleichzeitig auf allen Plattformen vorgenommen werden, sowie von allen Fehlerkorrekturen, die auf allen Plattformen implementiert werden.

Die Wiederverwendung von mehr als 100.000 Codezeilen und der Geschäftslogik, die unsere Rendering-Pipeline implementiert, war von entscheidender Bedeutung. Gleichzeitig können sie den Swift-Code mit anderen Toolchains kompatibel machen, um ihn bei Bedarf in Zukunft auf verschiedenen Plattformen wiederzuverwenden.

Iterative Produktentwicklung

Das Team verfolgte einen iterativen Ansatz, um Nutzern so schnell wie möglich ein Produkt anbieten zu können. Goodnotes begann mit einer schreibgeschützten Version des Produkts, mit der Nutzer jedes freigegebene Dokument auf jeder Plattform abrufen und lesen konnten. Über einen Link kann er auf dieselben Notizen zugreifen und sie lesen, die er auf seinem iPad geschrieben hat. In der nächsten Phase wurden Bearbeitungsfunktionen hinzugefügt, damit die plattformübergreifenden Versionen der iOS-Version entsprechen.

Zwei App-Screenshots, die den Wechsel von der schreibgeschützten Version zum voll funktionsfähigen Produkt symbolisieren.

Die Entwicklung der ersten Version des schreibgeschützten Produkts dauerte sechs Monate. Die folgenden neun Monate waren der ersten Reihe von Bearbeitungsfunktionen und dem UI-Bildschirm gewidmet, auf dem Sie alle Dokumente ansehen können, die Sie erstellt oder die jemand mit Ihnen geteilt hat. Außerdem konnten neue Funktionen der iOS-Plattform dank der SwiftWasm-Toolchain ganz einfach in das plattformübergreifende Projekt übertragen werden. Als Beispiel wurde ein neuer Stift entwickelt und durch Wiederverwendung von Tausenden Codezeilen plattformübergreifend implementiert.

Die Entwicklung dieses Projekts war eine unglaubliche Erfahrung und Goodnotes hat viel daraus gelernt. Deshalb konzentrieren sich die folgenden Abschnitte auf interessante technische Aspekte der Webentwicklung und die Verwendung von WebAssembly und Sprachen wie Swift.

Erste Hindernisse

Die Arbeit an diesem Projekt war aus vielen verschiedenen Blickwinkeln herausfordernd. Die erste Hürde, die das Team fand, bezog sich auf die SwiftWasm-Toolchain. Die Toolchain war eine große Hilfe für das Team, aber nicht der gesamte iOS-Code war mit Wasm kompatibel. Beispielsweise konnte Code im Zusammenhang mit E/A oder UI wie die Implementierung von Ansichten, API-Clients oder der Zugriff auf die Datenbank nicht wiederverwendet werden. Daher musste das Team bestimmte Teile der App umstrukturieren, um sie in der plattformübergreifenden Lösung wiederverwenden zu können. Die meisten PRs, die das Team erstellte, waren Refactorings zu abstrakten Abhängigkeiten, damit das Team sie später mithilfe von Dependency Injection oder anderen ähnlichen Strategien ersetzen konnte. Der iOS-Code enthielt ursprünglich rohe Geschäftslogik, die in Wasm implementiert werden konnte, sowie Code für Eingabe/Ausgabe und Benutzeroberfläche, der nicht in Wasm implementiert werden konnte, da Wasm diese Funktionen nicht unterstützt. Daher mussten der E/A- und UI-Code in TypeScript neu implementiert werden, sobald die Swift-Geschäftslogik für die plattformübergreifende Wiederverwendung bereit war.

Leistungsprobleme behoben

Als Goodnotes mit der Arbeit am Editor begann, stellte das Team einige Probleme mit der Bearbeitung fest und es wurden herausfordernde technologische Einschränkungen in unsere Roadmap aufgenommen. Das erste Problem betraf die Leistung. JavaScript ist eine einzeilige Sprache. Das bedeutet, dass es einen Aufrufstapel und einen Speicher-Heap hat. Der Code wird der Reihe nach ausgeführt und ein Codeabschnitt muss vollständig ausgeführt werden, bevor mit dem nächsten fortgefahren wird. Es ist synchron, was manchmal schädlich sein kann. Wenn beispielsweise die Ausführung einer Funktion etwas dauert oder auf etwas gewartet werden muss, wird alles in der Zwischenzeit eingefroren. Und genau dieses Problem mussten die Entwickler lösen. Die Auswertung bestimmter Pfade in unserer Codebasis im Zusammenhang mit der Rendering-Ebene oder anderen komplexen Algorithmen war für das Team ein Problem, da diese Algorithmen synchron waren und ihre Ausführung den Haupt-Thread blockierte. Das Goodnotes-Team hat sie neu geschrieben, um sie schneller zu machen, und einige davon umgestellt, um sie asynchron zu machen. Außerdem wurde eine Ertragsstrategie eingeführt, damit die App die Algorithmusausführung anhalten und später fortsetzen konnte. So konnte der Browser die Benutzeroberfläche aktualisieren und Frame-Ausfälle vermeiden. Das war für die iOS-Anwendung kein Problem, da sie Threads verwenden und diese Algorithmen im Hintergrund auswerten kann, während der Haupt-iOS-Thread die Benutzeroberfläche aktualisiert.

Eine weitere Herausforderung für das Entwicklungsteam bestand darin, eine Benutzeroberfläche, die auf HTML-Elementen basiert, die an das DOM angehängt sind, zu einer Dokumentbenutzeroberfläche mit einem Vollbild-Canvas zu migrieren. Zu Beginn des Projekts wurden alle Notizen und Inhalte, die sich auf ein Dokument beziehen, als Teil der DOM-Struktur mit HTML-Elementen angezeigt, wie es bei jeder anderen Webseite der Fall wäre. Irgendwann wurde jedoch zu einem Vollbild-Canvas gewechselt, um die Leistung auf Low-End-Geräten zu verbessern, indem die Zeit reduziert wurde, die der Browser für DOM-Aktualisierungen benötigt.

Das Engineering-Team hat die folgenden Änderungen identifiziert, die einige der aufgetretenen Probleme hätten reduzieren können, wenn sie zu Beginn des Projekts vorgenommen worden wären.

  • Entlasten Sie den Haupt-Thread, indem Sie häufig Webworker für arbeitsintensive Algorithmen verwenden.
  • Verwenden Sie von Anfang an exportierte und importierte Funktionen anstelle der JS-Swift-Interop-Bibliothek, um die Leistungseinbußen zu reduzieren, die durch das Verlassen des Wasm-Kontexts entstehen. Diese JavaScript-Interoperabilitätsbibliothek ist hilfreich, um Zugriff auf das DOM oder den Browser zu erhalten, aber sie ist langsamer als native Wasm-exportierte Funktionen.
  • Achten Sie darauf, dass der Code die Verwendung von OffscreenCanvas im Hintergrund zulässt, damit die App den Hauptthread auslagern und die gesamte Nutzung der Canvas API auf einen Webworker umverteilen kann, um die Leistung der Anwendung beim Erstellen von Notizen zu maximieren.
  • Verschieben Sie die gesamte Wasm-bezogene Ausführung auf einen Webworker oder sogar einen Pool von Webworkern, damit die App die Arbeitslast des Hauptthreads reduzieren kann.

Texteditor

Ein weiteres interessantes Problem bezog sich auf ein bestimmtes Tool, den Texteditor. Die iOS-Implementierung dieses Tools basiert auf NSAttributedString, einem kleinen Toolset, das intern RTF verwendet. Diese Implementierung ist jedoch nicht mit SwiftWasm kompatibel. Daher musste das plattformübergreifende Team zuerst einen benutzerdefinierten Parser basierend auf der RTF-Grammatik erstellen und später die Bearbeitung implementieren, indem RTF in HTML und umgekehrt umgewandelt wurde. In der Zwischenzeit begann das iOS-Team mit der Arbeit an der neuen Implementierung dieses Tools. Dabei wurde die Verwendung von RTF durch ein benutzerdefiniertes Modell ersetzt, damit die App auf allen Plattformen, die denselben Swift-Code verwenden, einen ansprechenden Text darstellen kann.

Der Texteditor von GoodNotes

Diese Herausforderung war einer der interessantesten Punkte in der Projekt-Roadmap, da sie iterativ anhand der Anforderungen der Nutzer gelöst wurde. Es war ein technisches Problem, das mit einem nutzerorientierten Ansatz gelöst wurde. Das Team musste einen Teil des Codes neu schreiben, um Text rendern zu können, und ermöglichte so in einer zweiten Version die Textbearbeitung.

Iterativ

Die Entwicklung des Projekts in den letzten zwei Jahren war unglaublich. Das Team begann mit der Arbeit an einer schreibgeschützten Version des Projekts und stellte Monate später eine brandneue Version mit vielen Bearbeitungsfunktionen vor. Um Codeänderungen häufig in der Produktion freizugeben, entschied sich das Team, Feature-Flags ausgiebig zu verwenden. Bei jeder Veröffentlichung konnte das Team neue Funktionen aktivieren und Codeänderungen veröffentlichen, mit denen neue Funktionen implementiert wurden, die die Nutzer erst Wochen später sehen würden. Das Team ist jedoch der Meinung, dass es etwas hätte verbessern können. Er ist der Meinung, dass die Einführung eines dynamischen Feature-Flag-Systems die Dinge beschleunigt hätte, da eine Neubereitstellung zum Ändern der Flag-Werte nicht mehr erforderlich wäre. Dies würde Goodnotes mehr Flexibilität bieten und auch die Bereitstellung der neuen Funktion beschleunigen, da Goodnotes die Projektbereitstellung nicht mit der Produktveröffentlichung verknüpfen müsste.

Offline arbeiten

Eine der wichtigsten Funktionen, an denen das Team gearbeitet hat, ist die Offlineunterstützung. Die Möglichkeit, Dokumente zu bearbeiten und zu ändern, ist eine Funktion, die Sie von einer solchen Anwendung erwarten würden. Das ist jedoch keine einfache Funktion, da Goodnotes die Gruppenarbeit unterstützt. Das bedeutet, dass alle Änderungen, die von verschiedenen Nutzern auf verschiedenen Geräten vorgenommen werden, auf jedem Gerät zu sehen sein sollten, ohne dass Nutzer Konflikte beheben müssen. Goodnotes hat dieses Problem vor langer Zeit gelöst, indem es im Hintergrund CRDTs verwendet. Dank dieser konfliktfreien replizierten Datentypen kann Goodnotes alle Änderungen kombinieren, die von einem Nutzer an einem Dokument vorgenommen wurden, und die Änderungen ohne Zusammenführungskonflikt zusammenführen. Die Verwendung von IndexedDB und der für Webbrowser verfügbare Speicherplatz waren ein wichtiger Faktor für die Offline-Gruppenarbeit im Web.

Die Goodnotes App funktioniert offline.

Außerdem verursacht das Öffnen der Goodnotes-Web-App aufgrund der Größe der Wasm-Binärdatei anfängliche Downloadkosten von etwa 40 MB. Ursprünglich hat das Goodnotes-Team ausschließlich auf den regulären Browsercache für das App-Bundle selbst und die meisten verwendeten API-Endpunkte gesetzt. Im Nachhinein hätte es jedoch früher von der zuverlässigeren Cache API und Service Workern profitieren können. Das Team hatte sich ursprünglich aufgrund der angenommenen Komplexität vor dieser Aufgabe gescheut, stellte aber am Ende fest, dass Workbox die Aufgabe viel weniger angsteinflößend machte.

Empfehlungen für die Verwendung von Swift im Web

Wenn Sie eine iOS-Anwendung mit viel Code haben, den Sie wiederverwenden möchten, machen Sie sich bereit, denn Sie stehen vor einer unglaublichen Reise. Bevor Sie beginnen, haben wir einige Tipps für Sie.

  • Prüfen Sie, welchen Code Sie wiederverwenden möchten. Wenn die Geschäftslogik Ihrer App auf der Serverseite implementiert ist, möchten Sie Ihren UI-Code wahrscheinlich wiederverwenden. Hier hilft Ihnen Wasm nicht weiter. Das Team hat sich kurz Tokamak angesehen, ein SwiftUI-kompatibles Framework zum Erstellen von Browser-Apps mit WebAssembly. Es war jedoch noch nicht ausgereift genug für die Anforderungen der App. Wenn Ihre App jedoch eine umfangreiche Geschäftslogik oder Algorithmen enthält, die im Clientcode implementiert sind, ist Wasm die beste Wahl.
  • Prüfen Sie, ob Ihre Swift-Codebasis bereit ist. Softwaredesignmuster für die UI-Ebene oder bestimmte Architekturen, die eine starke Trennung zwischen Ihrer UI-Logik und Ihrer Geschäftslogik schaffen, sind sehr praktisch, da Sie die Implementierung der UI-Ebene nicht wiederverwenden können. Die Clean Architecture oder die Grundsätze der hexagonalen Architektur sind ebenfalls von grundlegender Bedeutung, da Sie Abhängigkeiten für den gesamten IO-bezogenen Code einschleusen und bereitstellen müssen. Dies ist viel einfacher, wenn Sie diese Architekturen einhalten, in denen Implementierungsdetails als Abstrakte definiert und das Prinzip der Umkehr der Abhängigkeit intensiv verwendet wird.
  • Wasm bietet keinen UI-Code. Entscheiden Sie sich daher für das UI-Framework, das Sie für das Web verwenden möchten.
  • JSKit hilft Ihnen, Ihren Swift-Code in JavaScript einzubinden. Beachten Sie jedoch, dass die JS-zu-Swift-Brücke bei einem Hotpath möglicherweise teuer ist und Sie sie durch exportierte Funktionen ersetzen müssen. Weitere Informationen zur Funktionsweise von JSKit finden Sie in der offiziellen Dokumentation und im Artikel Dynamic Member Lookup in Swift, a hidden gem!.
  • Ob Sie Ihre Architektur wiederverwenden können, hängt von der Architektur Ihrer App und der von Ihnen verwendeten Bibliothek für den asynchronen Codeausführungsmechanismus ab. Muster wie MVVP oder die kompositionsfähige Architektur helfen Ihnen, Ihre Ansichtsmodelle und einen Teil der UI-Logik wiederzuverwenden, ohne die Implementierung an UIKit-Abhängigkeiten zu koppeln, die Sie nicht mit Wasm verwenden können. RXSwift und andere Bibliotheken sind möglicherweise nicht mit Wasm kompatibel. Beachten Sie dies, da Sie OpenCombine, async/await und Streams im Swift-Code von GoodNotes verwenden müssen.
  • Komprimieren Sie die Wasm-Binärdatei mit gzip oder Brotli. Beachten Sie, dass die Größe des Binärprogramms für klassische Webanwendungen recht groß sein wird.
  • Auch wenn Sie Wasm ohne die PWA verwenden können, sollten Sie mindestens einen Dienst-Worker einbinden, auch wenn Ihre Webanwendung kein Manifest hat oder Sie nicht möchten, dass der Nutzer sie installiert. Der Service Worker speichert und sendet die Wasm-Binärdatei und alle App-Ressourcen kostenlos, sodass Nutzer sie nicht jedes Mal herunterladen müssen, wenn sie Ihr Projekt öffnen.
  • Die Einstellung kann schwieriger sein als erwartet. Möglicherweise müssen Sie erfahrene Webentwickler mit etwas Erfahrung in Swift oder erfahrene Swift-Entwickler mit etwas Erfahrung im Web einstellen. Wenn Sie Generalisten mit etwas Erfahrung auf beiden Plattformen finden, wäre das super.

Ergebnisse

Ein Webprojekt mit einem komplexen Tech-Stack zu entwickeln und gleichzeitig an einem Produkt mit vielen Herausforderungen zu arbeiten, ist eine unglaubliche Erfahrung. Es wird schwer, aber es lohnt sich. Ohne diesen Ansatz hätte Goodnotes nie eine Version für Windows, Android, ChromeOS und das Web veröffentlichen können, während an neuen Funktionen für die iOS-App gearbeitet wurde. Dank dieses Technologie-Stacks und des Entwicklerteams von Goodnotes ist Goodnotes jetzt überall verfügbar. Das Team ist bereit, an den nächsten Herausforderungen zu arbeiten. Weitere Informationen zu diesem Projekt finden Sie in diesem Vortrag des GoodNotes-Teams auf der NSSpain 2023. Probieren Sie Goodnotes für das Web aus.