In den letzten zwei Jahren hat das Entwicklerteam von Goodnotes an einem Projekt gearbeitet, mit dem die erfolgreiche iPad-Notizen-App auch auf anderen Plattformen eingeführt werden soll. In dieser Fallstudie wird beschrieben, wie die iPad-App des Jahres 2022 auf Web, ChromeOS, Android und Windows mithilfe von Webtechnologien und WebAssembly mithilfe des Swift-Codes entstanden ist, an dem das Team seit mehr als zehn Jahren arbeitet.
Warum Goodnotes ins Web, Android und Windows kam
2021 war Goodnotes nur als App für iOS und iPad verfügbar. Das Engineering-Team von Goodnotes hat eine große technische Herausforderung angenommen: eine neue Version von Goodnotes für zusätzliche Betriebssysteme und Plattformen zu erstellen. Das Produkt sollte vollständig kompatibel mit der iOS-App sein und dieselben Notizen rendern. Notizen, die über einer PDF-Datei oder einem angehängten Bild erstellt werden, müssen äquivalent sein und dieselben Konturen aufweisen, die in der iOS-App angezeigt werden. Jeder hinzugefügte Strich sollte dem von iOS-Nutzern hinzugefügten Strich entsprechen, unabhängig von dem vom Nutzer verwendeten Tool, z. B. Stift, Textmarker, Füller, Formen oder Radiergummi.
Aufgrund der Anforderungen und der Erfahrung des Entwicklungsteams kam das Team schnell zu dem Schluss, dass die Wiederverwendung der Swift-Codebasis die beste Vorgehensweise ist, da sie bereits geschrieben und über viele Jahre hinweg gut getestet wurde. Aber warum sollten Sie nicht einfach die bereits bestehende iOS/iPad-Anwendung auf eine andere Plattform oder Technologie wie Flutter oder Compose Multiplatform übertragen? Für den Wechsel zu einer neuen Plattform bräuchte eine Umformulierung von Goodnotes. Dies kann einen Wettlauf zwischen der bereits implementierten iOS-App und einer zu erstellenden, aus null neuen Anwendungen hervorrufen oder dazu führen, dass die Entwicklung der vorhandenen Anwendung beendet wird, während die neue Codebasis auf dem neuesten Stand ist. 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 arbeitete und die Funktionsgleichheit schaffte.
Das Produkt hatte bereits eine Reihe interessanter Herausforderungen für iOS durch das Hinzufügen von Funktionen wie die folgenden gelöst:
- Rendering von Notizen.
- Synchronisierung von Dokumenten und Notizen.
- Konfliktlösung für Notizen mit konfliktfreien replizierten Datentypen
- Datenanalyse zum Bewerten von KI-Modellen.
- Inhaltssuche und Dokumentindexierung
- Benutzerdefiniertes Scrollen und Animationen
- Modellimplementierung für alle UI-Ebenen ansehen.
Alle davon wären für andere Plattformen wesentlich einfacher zu implementieren, wenn das Entwicklerteam die iOS-Codebasis so einrichten könnte, dass sie bereits für iOS- und iPad-Anwendungen funktioniert, und sie als Teil eines Projekts ausführen könnte, das Goodnotes als Windows-, Android- oder Webanwendungen ausliefern 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 mit dem Open-Source- und von der Community verwalteten Projekt SwiftWasm einen Prototyp mit Wasm erstellt. Mit SwiftWasm konnte das Goodnotes-Team unter Verwendung des bereits implementierten Swift-Codes eine Wasm-Binärdatei generieren. Dieses Binärprogramm könnte in eine Webseite eingebunden werden, die als Progressive Web Application für Android, Windows, ChromeOS und alle anderen Betriebssysteme versendet wird.
Ziel war es, Goodnotes als PWA zu veröffentlichen und im Store jeder Plattform aufführen zu können. Neben Swift, der bereits für iOS verwendeten Programmiersprache und WebAssembly zum Ausführen von Swift-Code im Web, wurden beim 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 Service Worker:Dies sind sehr wichtige Enabler für dieses Projekt, da das Team unsere App als Offline-App bereitstellen könnte, die wie jede andere iOS-App funktioniert. Sie können sie über den Store oder über den Browser selbst installieren.
- PWABuilder:Das Hauptprojekt, das von Goodnotes verwendet wird, um die PWA in eine native Windows-Binärdatei zu verpacken, damit das Team die App über den Microsoft Store vertreiben kann.
- Vertrauenswürdige Webaktivitäten:Die wichtigste Android-Technologie, mit der das Unternehmen unsere PWA als native Anwendung bereitstellt.
Die folgende Abbildung zeigt, was mit klassischem TypeScript und React implementiert wird 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, mit der das Team bei Bedarf das DOM in unserem Editorbildschirm aus unserem Swift-Code verarbeiten oder sogar einige browserspezifische APIs verwenden kann.
Warum Wasm und das Web?
Obwohl Wasm nicht offiziell von Apple unterstützt wird, war das Goodnotes-Engineering-Team aus folgenden Gründen der Meinung, dass dieser Ansatz die beste Entscheidung war:
- Die Wiederverwendung von mehr als 100.000 Codezeilen.
- Die Fähigkeit, das Kernprodukt weiterzuentwickeln und gleichzeitig einen Beitrag zu den plattformübergreifenden Apps zu leisten.
- Die Möglichkeiten, jede Plattform so schnell wie möglich mithilfe eines iterativen Entwicklungsprozesses zu erreichen.
- Die Kontrolle über das Rendern desselben Dokuments, ohne die gesamte Geschäftslogik zu duplizieren, und führt zu Unterschieden in unseren Implementierungen.
- Sie profitieren von allen gleichzeitig auf jeder Plattform vorgenommenen Leistungsverbesserungen und allen Fehlerkorrekturen, die auf jeder Plattform implementiert wurden.
Die Wiederverwendung von mehr als 100.000 Codezeilen und die Geschäftslogik, die unsere Rendering-Pipeline implementiert, war von entscheidender Bedeutung. Gleichzeitig kann der Swift-Code, wenn er mit anderen Toolchains kompatibel ist, diesen Code in Zukunft bei Bedarf auf anderen Plattformen wiederverwenden.
Iterative Produktentwicklung
Das Team verfolgte einen iterativen Ansatz, um Nutzern so schnell wie möglich etwas bereitzustellen. Goodnotes begann mit einer schreibgeschützten Version des Produkts, mit der Nutzer jedes freigegebene Dokument abrufen und von jeder Plattform aus lesen konnten. Allein mit einem Link können sie auf die Notizen, die sie auf ihrem iPad geschrieben haben, zugreifen und sie lesen. In der nächsten Phase wurden Bearbeitungsfunktionen hinzugefügt, mit denen die plattformübergreifenden Versionen der iOS-Version entsprechen.
Die Entwicklung der ersten Version des schreibgeschützten Produkts dauerte sechs Monate. In den folgenden neun Monaten ging es um die ersten Bearbeitungsfunktionen und den UI-Bildschirm, auf dem du alle Dokumente überprüfen kannst, die du erstellt hast oder die jemand für dich freigegeben hat. Außerdem konnten neue Funktionen der iOS-Plattform dank der SwiftWasm-Toolchain leicht in das plattformübergreifende Projekt übertragen werden. Beispielsweise wurde ein neuer Stifttyp entwickelt, der mithilfe Tausender Codezeilen einfach plattformübergreifend implementiert werden konnte.
Die Erstellung dieses Projekts war eine unglaubliche Erfahrung und Goodnotes hat viel daraus gelernt. Aus diesem Grund konzentrieren sich die folgenden Abschnitte auf interessante technische Aspekte der Webentwicklung und der Verwendung von WebAssembly und Sprachen wie Swift.
Anfängliche Hindernisse
Die Arbeit an diesem Projekt war aus vielen verschiedenen Blickwinkeln eine echte Herausforderung. Die erste Hürde, die das Team entdeckte, betraf die SwiftWasm-Toolchain. Die Toolchain war ein enormer Enabler für das Team, aber nicht der gesamte iOS-Code war mit Wasm kompatibel. Beispielsweise war Code in Bezug auf E/A oder UI – wie die Implementierung von Ansichten, API-Clients oder Zugriff auf die Datenbank – nicht wiederverwendbar. Daher musste das Team mit der Refaktorierung bestimmter Teile der Anwendung beginnen, um sie aus der plattformübergreifenden Lösung wiederverwenden zu können. Die meisten der vom Team erstellten PRs waren Refaktorierungen, um Abhängigkeiten zu refaktorieren, die das Team später durch Abhängigkeitsinjektion oder ähnliche Strategien ersetzen konnte. Der iOS-Code mischte ursprünglich rohe Geschäftslogik, die in Wasm mit Code implementiert werden konnte, der für die Eingabe/Ausgabe und die Benutzeroberfläche zuständig war, der in Wasm nicht implementiert werden konnte, da Wasm auch nicht unterstützt. Daher mussten E/A- und UI-Code in TypeScript neu implementiert werden, sobald die Swift-Geschäftslogik für die Wiederverwendung auf anderen Plattformen bereit war.
Leistungsprobleme gelöst
Als Goodnotes mit der Arbeit am Editor begann, erkannte das Team Probleme mit der Bearbeitung. Außerdem kamen schwierige technologische Einschränkungen in unsere Roadmap. Das erste Problem hatte mit der Leistung zu tun. JavaScript ist eine Single-Threaded-Sprache. Das bedeutet, dass sie einen Aufrufstack und einen Arbeitsspeicher-Heap hat. Der Code wird der Reihe nach ausgeführt und muss erst abgeschlossen werden, bevor zum nächsten Code gewechselt wird. Das ist synchron, kann aber manchmal schädlich sein. Wenn die Ausführung einer Funktion beispielsweise eine Weile dauert oder auf etwas warten muss, friert sie in der Zwischenzeit alles ein. Und genau das mussten die Engineers lösen. Die Auswertung bestimmter Pfade in unserer Codebasis, die sich auf die Rendering-Ebene oder andere komplexe Algorithmen beziehen, war für das Team ein Problem, da die Algorithmen synchron waren und ihre Ausführung den Hauptthread blockierte. Das Goodnotes-Team hat sie umformuliert, um sie schneller zu machen, und einige davon refaktoriert, um sie asynchron zu machen. Außerdem wurde eine Ertragsstrategie eingeführt, damit die Anwendung die Ausführung des Algorithmus stoppen und später fortsetzen konnte, sodass der Browser die Benutzeroberfläche aktualisieren konnte und keine Frames verloren gingen. Dies war kein Problem für die iOS-App, da sie Threads verwenden und diese Algorithmen im Hintergrund auswerten kann, während der iOS-Hauptthread die Benutzeroberfläche aktualisiert.
Eine weitere Lösung, die das Engineering-Team lösen musste, war die Migration einer UI, die auf HTML-Elementen basiert, die an das DOM angehängt sind, zu einer Dokument-UI, die auf einem Vollbild-Canvas basiert. Das Projekt begann, alle Notizen und Inhalte für ein Dokument als Teil der DOM-Struktur mit HTML-Elementen anzuzeigen, wie es jede andere Webseite tun würde. Später wurde es jedoch zu einem Vollbild-Canvas migriert, um die Leistung auf Low-End-Geräten zu verbessern, indem der Browser die Zeit für die Arbeit an DOM-Updates verkürzt hat.
Die folgenden Änderungen wurden vom Engineering-Team als Maßnahmen identifiziert, die einige der aufgetretenen Probleme reduziert hätten, wenn sie diese zu Beginn des Projekts vorgenommen hätten.
- Den Hauptthread stärker entlasten, indem Sie Web Worker für komplexe Algorithmen häufig verwenden
- Verwenden Sie seit Beginn exportierte und importierte Funktionen anstelle der JS-Swift-Interoperabilitätsbibliothek, damit die Auswirkungen auf die Leistung reduziert werden können, wenn Sie den Wasm-Kontext verlassen. Diese JavaScript-Interoperabilitätsbibliothek ist hilfreich, um auf das DOM oder den Browser zuzugreifen, sie ist jedoch langsamer als native exportierte Wasm-Funktionen.
- Achten Sie darauf, dass der Code die Nutzung von
OffscreenCanvas
im Hintergrund zulässt, damit die Anwendung den Hauptthread auslagern und die gesamte Nutzung der Canvas API auf einen Web Worker übertragen kann, der die Leistung der Anwendungen beim Schreiben von Notizen maximiert. - Verschieben Sie die gesamte Wasm-bezogene Ausführung auf einen Web Worker oder sogar einen Pool von Web-Workern, damit die Anwendung die Arbeitslast des Hauptthreads 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 RTF im Hintergrund verwendet. Diese Implementierung ist jedoch nicht mit SwiftWasm kompatibel. Daher musste das plattformübergreifende Team zuerst einen benutzerdefinierten Parser erstellen, der auf der RTF-Grammatik basiert. Später wurde die Bearbeitung implementiert, indem RTF in HTML umgewandelt wurde und umgekehrt. In der Zwischenzeit begann das iOS-Team mit der neuen Implementierung für dieses Tool, wobei die Verwendung von RTF durch ein benutzerdefiniertes Modell ersetzt wurde, damit die App formatierten Text für alle Plattformen, die denselben Swift-Code verwenden, nutzerfreundlich darstellen kann.
Diese Herausforderung war einer der interessantesten Punkte in der Projekt-Roadmap, da sie anhand der Anforderungen der Nutzer iterativ gelöst wurde. Es war ein Engineering-Problem, das mit einem nutzerorientierten Ansatz gelöst wurde, bei dem das Team einen Teil des Codes umschreiben musste, um Text rendern zu können, sodass die Textbearbeitung in einem zweiten Release ermöglicht wurde.
Iterative Releases
Die Entwicklung des Projekts in den letzten zwei Jahren war unglaublich. Das Team begann, an einer schreibgeschützten Version des Projekts zu arbeiten, und veröffentlichte Monate später eine ganz neue Version mit vielen Bearbeitungsfunktionen. Um häufig Codeänderungen für die Produktion zu veröffentlichen, beschloss das Team, Feature-Flags ausgiebig zu verwenden. Das Team kann für jeden Release neue Funktionen aktivieren und Änderungen am Code vornehmen, um neue Funktionen zu implementieren, die dem Nutzer Wochen später angezeigt werden. Das Team glaubt jedoch, dass es etwas verbessern könnte. Sie glaubt, dass die Einführung eines dynamischen Funktions-Flag-Systems dazu beigetragen hätte, den Vorgang zu beschleunigen, da so eine erneute Bereitstellung zum Ändern von Flag-Werten nicht mehr erforderlich wäre. Dies würde Goodnotes mehr Flexibilität geben und auch die Bereitstellung der neuen Funktion beschleunigen, da Goodnotes die Projektbereitstellung nicht mit dem Produktrelease verknüpfen müsste.
Offline arbeiten
Eine der Hauptfunktionen, an denen das Team gearbeitet hat, ist der Offline-Support. Die Möglichkeit, Dokumente zu bearbeiten und zu ändern, ist eine Funktion, die Sie von jeder Anwendung wie dieser erwarten würden. Dies ist jedoch keine einfache Funktion, da Goodnotes die Zusammenarbeit unterstützt. Das bedeutet, dass alle Änderungen, die von verschiedenen Nutzern auf verschiedenen Geräten vorgenommen werden, auf jedem Gerät übernommen werden sollten, ohne dass Nutzer aufgefordert werden, Konflikte zu lösen. Goodnotes hat dieses Problem schon vor langer Zeit mit CRDTs gelöst. Dank dieser konfliktfreien replizierten Datentypen ist Goodnotes in der Lage, alle Änderungen, die von einem Nutzer an einem Dokument vorgenommen wurden, zu kombinieren und die Änderungen ohne Zusammenführungskonflikte zusammenzuführen. Die Nutzung von IndexedDB und der für Webbrowser verfügbare Speicher waren ein wesentlicher Faktor für die Offline-Zusammenarbeit im Web.
Außerdem verursacht das Öffnen der Goodnotes-Webanwendung aufgrund der Wasm-Binärgröße eine anfängliche Download-Kosten von etwa 40 MB. Anfangs hat sich das Goodnotes-Team ausschließlich auf den regulären Browser-Cache für das App Bundle und die meisten der von ihm verwendeten API-Endpunkte verlassen, aber im Nachhinein hätte es früher von den zuverlässigeren Cache API- und Service Workern profitieren können. Das Team hat diese Aufgabe anfangs aufgrund der angenommenen Komplexität abgelehnt, stellte dann aber am Ende fest, dass Workbox sie deutlich weniger beängstigend macht.
Empfehlungen für die Verwendung von Swift im Web
Wenn Sie eine iOS-App mit viel Code haben, den Sie wiederverwenden möchten, sollten Sie sich darauf vorbereiten. Hier sind einige Tipps, die Sie vielleicht interessant finden werden, bevor Sie beginnen.
- Prüfen Sie, welchen Code Sie wiederverwenden möchten. Wenn die Geschäftslogik Ihrer App serverseitig implementiert ist, ist es wahrscheinlich, dass Sie Ihren UI-Code wiederverwenden würden. Wasm hilft Ihnen hier nicht weiter. Das Team sah sich kurz Tokamak an, ein mit SwiftUI kompatibles Framework zum Erstellen von Browseranwendungen mit WebAssembly. Dieses Framework war jedoch für die Anforderungen der Anwendung noch nicht ausgereift genug. Wenn Ihre Anwendung jedoch eine starke Geschäftslogik oder starke Algorithmen als Teil des Clientcodes implementiert, ist Wasm Ihre beste Wahl.
- Achten Sie darauf, dass 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 ermöglichen, sind sehr praktisch, da Sie die Implementierung der UI-Ebene nicht wiederverwenden können. Eine saubere Architektur oder Prinzipien der sechseckigen Architektur sind ebenfalls von grundlegender Bedeutung, da Sie Abhängigkeiten für den gesamten E/A-Code einschleusen und bereitstellen müssen. Dies ist einfacher, wenn Sie diese Architekturen verwenden, bei denen Implementierungsdetails als Abstraktionen definiert sind und das Abhängigkeitsinversionsprinzip häufig verwendet wird.
- Wasm stellt keinen UI-Code bereit. 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 integrieren. Beachten Sie jedoch, dass das Überqueren der JS-Swift-Brücke teuer sein kann, wenn Sie einen Hotpath haben und ihn durch exportierte Funktionen ersetzen müssen. Weitere Informationen dazu, wie JSKit im Hintergrund funktioniert, finden Sie in der offiziellen Dokumentation und im Post Dynamische Mitgliedersuche in Swift, ein verborgenes Juwel!.
- Ob Sie die Architektur wiederverwenden können, hängt von der Architektur ab, der Ihre Anwendung folgt, und der Mechanismus zur asynchronen Codeausführung, den Sie verwenden. Mithilfe von Mustern wie MVVP oder zusammensetzbaren Architekturen können Sie Ansichtsmodelle und einen Teil der UI-Logik wiederverwenden, ohne die Implementierung mit UIKit-Abhängigkeiten zu koppeln, die nicht mit Wasm verwendet werden können. RXSwift und andere Bibliotheken sind möglicherweise nicht mit Wasm kompatibel. Behalten Sie dies im Hinterkopf, 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. Denken Sie daran, dass die Binärdatei für klassische Webanwendungen recht groß sein wird.
- Auch wenn Sie Wasm ohne die PWA verwenden können, sollten Sie zumindest einen Service Worker angeben, auch wenn Ihre Web-App kein Manifest hat oder Sie nicht möchten, dass der Nutzer sie installiert. Der Service Worker speichert die Wasm-Binärdatei und alle Anwendungsressourcen kostenlos und stellt sie bereit, sodass der Nutzer sie nicht jedes Mal herunterladen muss, wenn er Ihr Projekt öffnet.
- Denken Sie daran, dass die Einstellung möglicherweise schwieriger sein kann als erwartet. Möglicherweise benötigen Sie starke Webentwickler mit Erfahrung mit Swift oder starke Swift-Entwickler mit ein wenig Erfahrung im Web. Es wäre toll, wenn Sie Multitalente mit Kenntnissen beider Plattformen finden könnten.
Ergebnisse
Ein Webprojekt mithilfe eines komplexen Technologie-Stacks zu erstellen und gleichzeitig an einem Produkt voller Herausforderungen zu arbeiten, ist ein unglaubliches Erlebnis. Es wird schwierig, aber es lohnt sich! Ohne diesen Ansatz hätte Goodnotes bei der Arbeit an neuen Funktionen für die iOS-Anwendung keine Version für Windows, Android, ChromeOS und das Web veröffentlichen können. Dank dieses Technologie-Stacks und des Engineering-Teams von Goodnotes ist Goodnotes nun überall verfügbar und das Team ist bereit, an den nächsten Herausforderungen zu arbeiten. Wenn Sie mehr über dieses Projekt erfahren möchten, können Sie sich einen Vortrag des Goodnotes-Teams beim NSSpain 2023 ansehen. Wir empfehlen Ihnen, Goodnotes für das Web auszuprobieren.