Überall Goodnotes

Goodnotes-Marketingbild, 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 Notiz-App für das iPad auf andere Plattformen zu bringen. In dieser Fallstudie wird beschrieben, wie die iPad-App des Jahres 2022 auf Web-, ChromeOS-, Android- und Windows-Plattformen verfügbar gemacht wurde. Dabei wurden Webtechnologien und WebAssembly verwendet und derselbe Swift-Code wiederverwendet, an dem das Team seit mehr als zehn Jahren arbeitet.

Goodnotes-Logo

Warum Goodnotes für Web, Android und Windows verfügbar ist

2021 war Goodnotes nur als App für iOS und iPad verfügbar. Das Entwicklerteam von Goodnotes nahm eine große technische Herausforderung an: die Entwicklung einer neuen Version von Goodnotes für zusätzliche Betriebssysteme und Plattformen. Das Produkt sollte vollständig mit der iOS-Anwendung kompatibel sein und dieselben Notizen rendern. Alle Notizen, die auf einem PDF gemacht werden, oder alle angehängten Bilder sollten gleich sein und dieselben Striche wie in der iOS-App enthalten. Jeder hinzugefügte Strich sollte dem entsprechen, was iOS-Nutzer erstellen können, unabhängig vom verwendeten Tool, z. B. Stift, Textmarker, Füllfederhalter, 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 gründlich getestet worden war. Warum sollte man die bereits vorhandene iOS-/iPad-Anwendung nicht einfach auf eine andere Plattform oder Technologie wie Flutter oder Compose Multiplatform portieren? Ein Wechsel zu einer neuen Plattform würde eine Neuentwicklung von Goodnotes erfordern. Dies könnte zu einem Wettlauf zwischen der bereits implementierten iOS-Anwendung und einer von Grund auf neu zu entwickelnden Anwendung führen oder dazu, dass die Entwicklung der bestehenden Anwendung gestoppt wird, bis die neue Codebasis aufgeholt hat. Wenn Goodnotes den Swift-Code wiederverwenden könnte, könnte das Team von neuen Funktionen profitieren, die vom iOS-Team implementiert wurden, während das plattformübergreifende Team an den Grundlagen der App und der Erreichbarkeitsfunktion arbeitete.

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

  • Notizen werden gerendert.
  • Synchronisierung von Dokumenten und Notizen
  • Konfliktlösung für Notizen mit Conflict-Free Replicated Data Types (konfliktfreie replizierte Datentypen).
  • Datenanalyse zur Bewertung von KI-Modellen.
  • Inhaltssuche und Dokumentindexierung.
  • Benutzerdefiniertes Scrollen und Animationen.
  • Sehen Sie sich die Modellimplementierung für alle UI-Ebenen an.

Alle wären für andere Plattformen viel einfacher zu implementieren, wenn das Entwicklerteam den iOS-Code bereits für iOS- und iPad-Anwendungen zum Laufen bringen und als Teil eines Projekts ausführen könnte, das Goodnotes als Windows-, Android- oder Webanwendung bereitstellen könnte.

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-Projekt SwiftWasm erstellt, das von der Community verwaltet wird. Mit SwiftWasm konnte das Goodnotes-Team ein Wasm-Binärprogramm mit dem gesamten bereits implementierten Swift-Code generieren. Dieses Binärprogramm kann in eine Webseite eingebunden werden, die als Progressive Web-App für Android, Windows, ChromeOS und alle anderen Betriebssysteme ausgeliefert wird.

Die Einführung von Goodnotes beginnt mit Chrome, dann Windows, gefolgt von Android und anderen Plattformen wie Linux am Ende, alles basierend auf der PWA.

Ziel war es, Goodnotes als PWA zu veröffentlichen und in den Stores aller Plattformen anbieten zu können. Neben Swift, der Programmiersprache, die bereits für iOS verwendet wird, und WebAssembly, das zum Ausführen von Swift-Code im Web verwendet wird, wurden im Projekt die folgenden Technologien eingesetzt:

  • TypeScript:Die am häufigsten verwendete Programmiersprache für Webtechnologien.
  • React und webpack:Das beliebteste Framework und der beliebteste Bundler für das Web.
  • PWAs und Service Worker:Diese Technologien waren für das Projekt von großer Bedeutung, da das Team die App als Offlineanwendung bereitstellen konnte, die wie jede andere iOS-App funktioniert und über den Store oder den Browser selbst installiert werden kann.
  • PWABuilder:Das Hauptprojekt, das Goodnotes verwendet, um die PWA in eine native Windows-Binärdatei zu verpacken, damit das Team unsere App über den Microsoft Store vertreiben kann.
  • Vertrauenswürdige Web-Aktivitäten:Die wichtigste Android-Technologie, die das Unternehmen verwendet, um unsere PWA im Hintergrund als native Anwendung zu verteilen.

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

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

App-Screenshots auf Mobilgeräten und Computern, auf denen die Zeichenbereiche, die von Wasm gesteuert werden, und die UI-Bereiche, die von React gesteuert werden, zu sehen sind.

Warum Wasm und das Web verwenden?

Obwohl Wasm nicht offiziell von Apple unterstützt wird, hat sich das Goodnotes-Entwicklungsteam aus folgenden Gründen für diesen Ansatz entschieden:

  • Die Wiederverwendung von mehr als 100.000 Codezeilen.
  • Die Möglichkeit, die Entwicklung des Kernprodukts fortzusetzen und gleichzeitig zu den plattformübergreifenden Apps beizutragen.
  • Die Vorteile, die sich aus der Möglichkeit ergeben, mit einem iterativen Entwicklungsprozess so schnell wie möglich auf jeder Plattform präsent zu sein.
  • Wir möchten dasselbe Dokument rendern können, ohne die gesamte Geschäftslogik zu duplizieren und Unterschiede in unseren Implementierungen einzuführen.
  • Sie profitieren gleichzeitig von allen Leistungsverbesserungen und Fehlerkorrekturen, die auf den einzelnen Plattformen vorgenommen wurden.

Die Wiederverwendung von mehr als 100.000 Codezeilen und der Geschäftslogik zur Implementierung unserer Rendering-Pipeline war von grundlegender Bedeutung. Gleichzeitig wird der Swift-Code durch die Kompatibilität mit anderen Toolchains wiederverwendbar und kann bei Bedarf in Zukunft auf verschiedenen Plattformen eingesetzt werden.

Iterative Produktentwicklung

Das Team hat einen iterativen Ansatz gewählt, um den Nutzern so schnell wie möglich etwas zur Verfügung zu stellen. Goodnotes begann mit einer schreibgeschützten Version des Produkts, in der Nutzer jedes freigegebene Dokument aufrufen und auf jeder Plattform lesen konnten. Mit einem Link können sie auf die Notizen zugreifen und sie lesen, die sie auf ihrem iPad geschrieben haben. In der nächsten Phase wurden Bearbeitungsfunktionen hinzugefügt, um die plattformübergreifenden Versionen an die iOS-Version anzugleichen.

Zwei App-Screenshots, die den Übergang vom Nur-Lese-Modus zum Produkt mit allen Funktionen symbolisieren.

Die Entwicklung der ersten Version des schreibgeschützten Produkts dauerte sechs Monate. In den folgenden neun Monaten wurden die ersten Bearbeitungsfunktionen und die Benutzeroberfläche entwickelt, auf der Sie alle Dokumente sehen können, die Sie erstellt haben oder die mit Ihnen geteilt wurden. Außerdem ließen sich neue Funktionen der iOS-Plattform dank der SwiftWasm-Toolchain problemlos in das plattformübergreifende Projekt portieren. So wurde beispielsweise ein neuer Stifttyp erstellt und plattformübergreifend implementiert, indem Tausende von Codezeilen wiederverwendet wurden.

Die Entwicklung dieses Projekts war eine unglaubliche Erfahrung und Goodnotes hat viel daraus gelernt. In den folgenden Abschnitten werden daher interessante technische Aspekte der Webentwicklung und der Verwendung von WebAssembly und Sprachen wie Swift behandelt.

Anfängliche Hindernisse

Die Arbeit an diesem Projekt war aus vielen verschiedenen Blickwinkeln eine große Herausforderung. Das erste Hindernis, auf das das Team stieß, betraf 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 war Code im Zusammenhang mit E/A oder UI, wie die Implementierung von Ansichten, API-Clients oder der Zugriff auf die Datenbank, nicht wiederverwendbar. Das Team musste daher mit dem Refactoring bestimmter Teile der App beginnen, um sie in der plattformübergreifenden Lösung wiederverwenden zu können. Die meisten PRs, die das Team erstellt hat, waren Refactorings, um Abhängigkeiten zu abstrahieren, damit das Team sie später durch Dependency Injection oder ähnliche Strategien ersetzen konnte. Der ursprüngliche iOS-Code enthielt sowohl rohe Geschäftslogik, die in Wasm implementiert werden konnte, als auch Code für Ein-/Ausgabe und Benutzeroberfläche, der nicht in Wasm implementiert werden konnte, da Wasm beides nicht unterstützt. Daher mussten Ein- und Ausgabecode sowie UI-Code in TypeScript neu implementiert werden, sobald die Swift-Geschäftslogik plattformübergreifend wiederverwendet werden konnte.

Leistungsprobleme behoben

Als Goodnotes mit der Entwicklung des Editors begann, stellte das Team einige Probleme mit der Bearbeitung fest. Außerdem wurden herausfordernde technische Einschränkungen in unsere Roadmap aufgenommen. Das erste Problem betraf die Leistung. JavaScript ist eine Single-Thread-Sprache. Das bedeutet, dass es einen Callstack und einen Memory Heap gibt. Er führt Code in der Reihenfolge aus und muss die Ausführung eines Codeabschnitts abschließen, bevor er mit dem nächsten fortfährt. Es ist synchron, aber manchmal kann das schädlich sein. Wenn eine Funktion beispielsweise eine Weile für die Ausführung benötigt oder auf etwas warten muss, wird in der Zwischenzeit alles eingefroren. Und genau das mussten die Entwickler lösen. Die Bewertung einiger spezifischer Pfade in unserer Codebasis, die sich auf die Rendering-Ebene oder andere komplexe Algorithmen beziehen, war für das Team ein Problem, da diese Algorithmen synchron waren und ihre Ausführung den Hauptthread blockierte. Das Goodnotes-Team hat sie neu geschrieben, um sie schneller zu machen, und einige von ihnen refaktoriert, um sie asynchron zu machen. Außerdem haben sie eine Ertragsstrategie eingeführt, damit die App die Ausführung des Algorithmus beenden und später fortsetzen kann. So kann der Browser die Benutzeroberfläche aktualisieren und Frame-Drops vermeiden. Für die iOS-Anwendung war das kein Problem, da sie Threads verwenden und diese Algorithmen im Hintergrund ausführen kann, während der Haupt-iOS-Thread die Benutzeroberfläche aktualisiert.

Eine weitere Herausforderung für das Entwicklungsteam war die Migration einer Benutzeroberfläche, die auf HTML-Elementen basiert, die an das DOM angehängt sind, zu einer Dokument-Benutzeroberfläche, die auf einem Vollbild-Canvas basiert. Im Projekt wurden zunächst alle Notizen und Inhalte, die sich auf ein Dokument beziehen, als Teil der DOM-Struktur mit HTML-Elementen wie auf jeder anderen Webseite angezeigt. Später wurde jedoch auf einen Vollbild-Canvas umgestellt, 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 als Möglichkeiten identifiziert, mit denen einige der aufgetretenen Probleme hätten reduziert werden können, wenn sie zu Beginn des Projekts vorgenommen worden wären.

  • Lagern Sie den Haupt-Thread aus, indem Sie Web-Worker häufig für rechenintensive Algorithmen verwenden.
  • Verwenden Sie von Anfang an exportierte und importierte Funktionen anstelle der JS-Swift-Interop-Bibliothek, um die Leistungseinbußen beim Verlassen des Wasm-Kontexts zu verringern. Diese JavaScript-Interop-Bibliothek ist hilfreich, um auf das DOM oder den Browser zuzugreifen, ist aber langsamer als native Wasm-exportierte Funktionen.
  • Der Code muss die Verwendung von OffscreenCanvas im Hintergrund ermöglichen, damit die App den Hauptthread entlasten und die gesamte Verwendung der Canvas API in einen Web-Worker verschieben kann. So wird die Leistung von Anwendungen beim Schreiben von Notizen maximiert.
  • Verschieben Sie die gesamte Wasm-bezogene Ausführung in einen Web-Worker oder sogar in einen Pool von Web-Workern, damit die App die Arbeitslast des Haupt-Threads reduzieren kann.

Der Texteditor

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

Der Goodnotes-Texteditor.

Diese Herausforderung war einer der interessantesten Punkte im Projektplan, da sie iterativ auf der Grundlage der Nutzeranforderungen gelöst wurde. Es handelte sich um 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. Daher wurde die Textbearbeitung in einer zweiten Version aktiviert.

Iterative Releases

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 veröffentlichte Monate später eine brandneue Version mit vielen Bearbeitungsfunktionen. Um Codeänderungen häufig in der Produktion zu veröffentlichen, hat sich das Team entschieden, Feature-Flags umfassend zu nutzen. Bei jeder Veröffentlichung konnte das Team neue Funktionen aktivieren und auch Codeänderungen veröffentlichen, die neue Funktionen implementierten, die der Nutzer erst Wochen später sehen würde. Allerdings gibt es etwas, das das Team hätte besser machen können. Sie sind der Meinung, dass ein dynamisches Feature-Flag-System die Dinge beschleunigt hätte, da kein erneutes Bereitstellen erforderlich gewesen wäre, um Flag-Werte zu ändern. Dadurch hätte Goodnotes mehr Flexibilität und die Bereitstellung der neuen Funktion würde beschleunigt, 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. Da Goodnotes die Zusammenarbeit unterstützt, ist das jedoch keine einfache Funktion. Das bedeutet, dass alle Änderungen, die von verschiedenen Nutzern auf verschiedenen Geräten vorgenommen werden, auf jedem Gerät angezeigt werden sollten, ohne dass Nutzer Konflikte lösen müssen. Goodnotes hat dieses Problem schon vor langer Zeit mit CRDTs gelöst. Dank dieser konfliktfreien replizierten Datentypen kann Goodnotes alle Änderungen, die von einem beliebigen Nutzer an einem beliebigen Dokument vorgenommen wurden, kombinieren und zusammenführen, ohne dass es zu Konflikten kommt. Die Verwendung von IndexedDB und der für Webbrowser verfügbare Speicherplatz haben die kollaborative Offline-Nutzung im Web erheblich erleichtert.

Die Goodnotes App funktioniert offline.

Außerdem führt das Öffnen der Goodnotes Web-App zu einem anfänglichen Download von etwa 40 MB aufgrund der Größe des Wasm-Binärprogramms. Anfangs verließ sich das Goodnotes-Team ausschließlich auf den regulären Browsercache für das App-Bundle selbst und die meisten der verwendeten API-Endpunkte. Im Nachhinein hätte es jedoch von der zuverlässigeren Cache API und Service Workern profitiert. Das Team scheute sich anfangs vor dieser Aufgabe, da sie als sehr komplex galt. Am Ende stellte sich jedoch heraus, dass Workbox die Aufgabe deutlich weniger schwierig machte.

Empfehlungen für die Verwendung von Swift im Web

Wenn Sie eine iOS-Anwendung mit viel Code haben, den Sie wiederverwenden möchten, sollten Sie sich darauf vorbereiten, denn es erwartet Sie eine unglaubliche Reise. Hier sind einige Tipps, die für Sie interessant sein könnten, bevor Sie beginnen.

  • Prüfen Sie, welchen Code Sie wiederverwenden möchten. Wenn die Geschäftslogik Ihrer App serverseitig implementiert ist, möchten Sie wahrscheinlich Ihren UI-Code wiederverwenden. Wasm kann Ihnen dabei nicht helfen. Das Team hat sich kurz Tokamak angesehen, ein SwiftUI-kompatibles Framework zum Erstellen von Browser-Apps mit WebAssembly, aber es war nicht ausgereift genug für die Anforderungen der App. Wenn Ihre App jedoch eine starke Geschäftslogik oder Algorithmen hat, die als Teil des Clientcodes implementiert sind, ist Wasm die beste Lösung.
  • Stellen Sie sicher, dass Ihre Swift-Codebasis bereit ist. Software-Designmuster für die UI-Ebene oder bestimmte Architekturen, die eine starke Trennung zwischen Ihrer UI-Logik und Ihrer Geschäftslogik schaffen, sind sehr nützlich, da Sie die Implementierung der UI-Ebene nicht wiederverwenden können. Die Prinzipien der Clean Architecture oder der hexagonalen Architektur sind ebenfalls von grundlegender Bedeutung, da Sie Abhängigkeiten für den gesamten E/A-bezogenen Code einfügen und bereitstellen müssen. Das ist viel einfacher, wenn Sie diese Architekturen verwenden, bei denen Implementierungsdetails als Abstraktionen definiert sind und das Prinzip der Abhängigkeitsinversion stark genutzt wird.
  • Wasm enthält keinen UI-Code. Entscheiden Sie sich daher für das UI-Framework, das Sie für das Web verwenden möchten.
  • Mit JSKit können Sie Ihren Swift-Code in JavaScript einbinden. Wenn Sie jedoch einen Hotpath haben, kann das Überqueren der JS–Swift-Brücke teuer sein. In diesem Fall müssen Sie sie durch exportierte Funktionen ersetzen. Weitere Informationen zur Funktionsweise von JSKit finden Sie in der offiziellen Dokumentation und im Beitrag 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 die asynchrone Codeausführung ab. Muster wie MVVP oder eine zusammensetzbare Architektur helfen Ihnen, Ihre Viewmodels 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. Das sollten Sie berücksichtigen, 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 Service Worker einfügen, selbst wenn Ihre Web-App kein Manifest hat oder Sie nicht möchten, dass der Nutzer sie installiert. Der Service Worker speichert und stellt das Wasm-Binärprogramm kostenlos sowie alle App-Ressourcen bereit, sodass der Nutzer sie nicht jedes Mal herunterladen muss, wenn er Ihr Projekt öffnet.
  • Die Einstellung von Mitarbeitern kann schwieriger sein als erwartet. Möglicherweise müssen Sie erfahrene Webentwickler mit Swift-Kenntnissen oder erfahrene Swift-Entwickler mit Webkenntnissen einstellen. Wenn Sie Generalist-Entwickler mit Kenntnissen auf beiden Plattformen finden, wäre das ideal.

Zusammenfassung

Ein Webprojekt mit einem komplexen Technologie-Stack zu entwickeln und gleichzeitig an einem Produkt zu arbeiten, das viele Herausforderungen mit sich bringt, ist eine unglaubliche Erfahrung. Es wird hart, aber es lohnt sich. Ohne diesen Ansatz hätte Goodnotes niemals eine Version für Windows, Android, ChromeOS und das Web veröffentlichen können, während gleichzeitig an neuen Funktionen für die iOS-Anwendung gearbeitet wurde. Dank dieses Technologie-Stacks und des Entwicklerteams von Goodnotes ist Goodnotes jetzt überall verfügbar und das Team ist bereit, sich den nächsten Herausforderungen zu stellen. Wenn Sie mehr über dieses Projekt erfahren möchten, können Sie sich einen Vortrag des Goodnotes-Teams auf der NSSpain 2023 ansehen. Probieren Sie Goodnotes für Web aus!