Notatki w każdym miejscu

Obraz marketingowy Goodnotes przedstawiający kobietę korzystającą z aplikacji na iPadzie.

Przez ostatnie 2 lata zespół inżynierów Goodnotes pracował nad projektem przeniesienia popularnej aplikacji do tworzenia notatek na iPada na inne platformy. Ten przypadek użycia opisuje, jak aplikacja na iPada, która w 2022 roku została wybrana jako „Aplikacja roku”, trafiła na platformy internetowe, ChromeOS, Android i Windows, korzystając z technologii internetowych i WebAssembly, a także używając ponownie kodu Swift, nad którym zespół pracował przez ponad 10 lat.

Logo Goodnotes

Dlaczego Goodnotes pojawił się w przeglądarce, na Androidzie i Windowsie

W 2021 r. Goodnotes był dostępny tylko jako aplikacja na iOS i iPada. Zespół inżynierów Goodnotes podjął się ogromnego wyzwania technicznego: stworzenia nowej wersji Goodnotes, ale dla dodatkowych systemów operacyjnych i platform. Produkt powinien być w pełni zgodny z aplikacją na iOS i renderować te same notatki. Wszystkie notatki dodane na dokumencie PDF lub dowolne załączone obrazy powinny być takie same i mieć takie same linie jak w aplikacji na iOS. Dodane pociągnięcia powinny być takie same jak te, które użytkownicy iOS mogą tworzyć niezależnie od narzędzia, którego używają (np. pióra, flamastra, pióra wiecznego, kształtów lub gumki).

Podgląd aplikacji Goodnotes z odręcznymi notatkami i szkicami

Na podstawie wymagań i doświadczeń zespołu inżynierów szybko doszli do wniosku, że najlepszym rozwiązaniem będzie ponowne użycie kodu źródłowego Swift, który został już napisany i dobrze przetestowany przez wiele lat. Ale dlaczego nie przenieść już istniejącej aplikacji na iOS/iPada na inną platformę lub technologię, taką jak Flutter czy Compose Multiplatform? Przejście na nową platformę wymagałoby przepisania Goodnotes. Może to spowodować wyścig deweloperów między już wdrożonym i nowym, tworzonym od podstaw, oprogramowaniem na iOS albo zmuszą Cię do przerwania prac nad istniejącym oprogramowaniem, dopóki nowa baza kodu nie zostanie do niego dopasowana. Gdyby Goodnotes mógł ponownie użyć kodu Swift, zespół mógłby korzystać z nowych funkcji zaimplementowanych przez zespół iOS, podczas gdy zespół platformy wieloplatformowej pracowałby nad podstawami aplikacji i uzyskiwaniem równości funkcji.

W przypadku iOS udało nam się już rozwiązać kilka ciekawych problemów, aby dodać takie funkcje jak:

  • Renderowanie notatek.
  • Synchronizacja dokumentów i notatek.
  • Rozwiązywanie konfliktów w przypadku notatek przy użyciu typów danych replikowanych bez konfliktów.
  • Analiza danych na potrzeby oceny modelu AI.
  • wyszukiwanie treści i indeksowanie dokumentów;
  • niestandardowe animacje i przewijanie
  • Wyświetl implementację modelu dla wszystkich warstw interfejsu użytkownika.

Wszystkie z nich można byłoby łatwiej wdrożyć na innych platformach, gdyby zespół inżynierów mógł wykorzystać kod źródłowy iOS w przypadku aplikacji na iOS i iPada, a także uruchomić go w ramach projektu Goodnotes, który mógłby udostępniać aplikacje na Windows, Androida i internet.

Stos technologiczny Goodnotes

Na szczęście istnieje sposób na ponowne użycie kodu Swift w internecie – WebAssembly (Wasm). Goodnotes stworzył prototyp za pomocą Wasm w ramach projektu SwiftWasm, który jest oparty na otwartym kodzie i utrzymywany przez społeczność. Dzięki SwiftWasm zespół Goodnotes mógł wygenerować plik binarny Wasm, korzystając z całej implementacji kodu Swift. Plik binarny może być zawarty na stronie internetowej dostarczanej jako progresywna aplikacja internetowa na Androida, Windowsa, ChromeOS i inne systemy operacyjne.

Sekwencja wdrażania Goodnotes zaczyna się od Chrome, potem Windows, a następnie Android i inne platformy, takie jak Linux, na których ostatecznie zacznie działać PWA.

Celem było udostępnienie Goodnotes jako PWA i możliwość umieszczenia go w sklepie każdej platformy. Oprócz Swift, czyli języka programowania używanego już w iOS, oraz WebAssembly, który służy do wykonywania kodu Swift w internecie, projekt korzystał z tych technologii:

  • TypeScript: najczęściej używany język programowania w przypadku technologii internetowych.
  • React i webpack: najpopularniejsze frameworki i narzędzia do tworzenia pakietów dla internetu.
  • PWA i usługi w tle: to ogromne ułatwienie w ramach tego projektu, ponieważ nasz zespół mógł udostępnić aplikację jako aplikację offline, która działa jak każda inna aplikacja na iOS. Można ją zainstalować ze sklepu lub z samej przeglądarki.
  • PWABuilder: główny projekt firmy Goodnotes, który służy do zapakowania aplikacji PWA w rodzaj pliku binarnego na potrzeby systemu Windows, aby zespół mógł dystrybuować naszą aplikację w Microsoft Store.
  • Zaufane działania w internecie: najważniejsza technologia Androida, której firma używa do dystrybucji naszej aplikacji internetowej jako natywnej aplikacji.

Zespół techniczny Goodnotes korzysta z technologii Swift, Wasm, React i PWA.

Na poniższym rysunku widać, co jest implementowane za pomocą klasycznego TypeScript i React, a co za pomocą SwiftWasm i zwykłego JavaScript, Swift i WebAssembly. Ta część projektu korzysta z JSKit, biblioteki interoperacyjności JavaScript dla Swift i WebAssembly, której zespół używa do obsługi DOM na ekranie edytora z naszego kodu Swift, a także do korzystania z niektórych interfejsów API przeznaczonych dla przeglądarek.

Zrzuty ekranu aplikacji na urządzeniach mobilnych i komputerach, na których widać obszary rysowania obsługiwane przez Wasm oraz obszary interfejsu obsługiwane przez React.

Dlaczego warto korzystać z Wasm i sieci?

Chociaż Wasm nie jest oficjalnie obsługiwany przez Apple, zespół inżynierów Goodnotes uznał, że jest to najlepsze rozwiązanie z tych powodów:

  • ponowne użycie ponad 100 tys. wierszy kodu;
  • Możliwość dalszego rozwijania podstawowej usługi przy jednoczesnym przyczynianiu się do tworzenia aplikacji wieloplatformowych.
  • Możliwość jak najszybszego udostępnienia aplikacji na wszystkich platformach dzięki iteratywnemu procesowi rozwoju.
  • możliwość renderowania tego samego dokumentu bez powielania całej logiki biznesowej i wprowadzania różnic w naszych implementacjach.
  • Korzystanie ze wszystkich ulepszeń wydajności wprowadzonych na wszystkich platformach w tym samym czasie (oraz ze wszystkich poprawek błędów wprowadzonych na wszystkich platformach).

Ponowne użycie ponad 100 tysięcy linii kodu i logiki biznesowej implementującej nasz łańcuch renderowania było kluczowe. Jednocześnie dzięki temu, że kod Swift jest zgodny z innymi zestawami narzędzi, deweloperzy mogą w przyszłości, w razie potrzeby, używać tego kodu na różnych platformach.

Iteracyjny rozwój produktu

Zespół zastosował podejście iteracyjne, aby jak najszybciej udostępnić użytkownikom nowe funkcje. Goodnotes zaczęło się od wersji tylko do odczytu, w której użytkownicy mogli pobrać dowolny udostępniony dokument i przeczytać go na dowolnej platformie. Dzięki linkowi będą mogli uzyskać dostęp do tych samych notatek na iPadzie. W następnej fazie dodaliśmy funkcje edycji, aby wersje na różne platformy były takie same jak wersja na iOS.

2 zrzuty ekranu aplikacji przedstawiające przejście z wersji tylko do odczytu do wersji z pełną funkcjonalnością

Pierwsza wersja produktu tylko do odczytu została opracowana w ciągu 6 miesięcy, a kolejne 9 miesięcy poświęcono na opracowanie pierwszej grupy funkcji edycji i ekranu interfejsu, na którym można sprawdzić wszystkie dokumenty utworzone przez Ciebie lub udostępnione przez kogoś innego. Co więcej, dzięki łańcuchowi narzędzi SwiftWasm łatwo było przenieść nowe funkcje platformy iOS do projektu wieloplatformowego. Na przykład został utworzony nowy typ pióra i łatwo zaimplementowano go na różnych platformach, wykorzystując tysiące linii kodu.

Tworzenie tego projektu było niesamowitym doświadczeniem, a Goodnotes wiele się z niego nauczył. Dlatego w następnych sekcjach skupimy się na ciekawych aspektach technicznych związanych z tworzeniem stron internetowych oraz z korzystaniem z WebAssembly i języków takich jak Swift.

Początkowe przeszkody

Praca nad tym projektem była bardzo wymagająca pod wieloma względami. Pierwsza przeszkoda, na jaką natrafił zespół, była związana z zestawem narzędzi SwiftWasm. Narzędzie to było dla zespołu bardzo przydatne, ale nie cały kod iOS był zgodny z Wasm. Na przykład kod związany z wejściem/wyjściem lub interfejsem użytkownika, np. implementacja widoków, klientów interfejsu API czy dostęp do bazy danych, nie nadawał się do ponownego użycia, więc zespół musiał rozpocząć refaktoryzację poszczególnych części aplikacji, aby móc używać ich w rozwiązaniu wieloplatformowym. Większość PR-ów utworzonych przez zespół była wynikiem refaktoryzacji abstrakcyjnych zależności, aby można je było później zastąpić za pomocą wstrzyknięcia zależności lub innych podobnych strategii. Kod na iOS-a pierwotnie mieszał surową logikę biznesową, którą można było zaimplementować w Wasm, z kodem odpowiedzialnym za dane wejściowe/wyjściowe i interfejs użytkownika, którego nie można było zaimplementować w Wasm, ponieważ Wasm nie obsługuje żadnego z tych elementów. Dlatego, gdy logika biznesowa w Swift była gotowa do ponownego użycia na różnych platformach, kod IO i UI trzeba było ponownie zaimplementować w TypeScript.

Rozwiązanie problemów z wydajnością

Gdy Goodnotes zaczął pracować nad edytorem, zespół zidentyfikował pewne problemy związane z edycją i wyzwaniami, które pojawiły się w naszej ścieżce rozwoju. Pierwszy problem dotyczył wydajności. JavaScript jest językiem jednowątkowym. Oznacza to, że ma 1 stół wywołań i 1 kupę pamięci. Kod jest wykonywany w kolejności i musi zostać ukończony, zanim przejdzie do następnego. Jest synchroniczny, ale czasami może być szkodliwy. Jeśli na przykład wykonanie funkcji zajmuje trochę czasu lub musi na coś poczekać, wszystko jest wstrzymane. I właśnie to inżynierowie musieli rozwiązać. Ocena niektórych ścieżek w naszym kodzie źródłowym związanych z poziomem renderowania lub innymi złożonymi algorytmami była dla zespołu problemem, ponieważ te algorytmy były synchroniczne, a ich wykonywanie blokowało główny wątek. Zespół Goodnotes przepisał je, aby działały szybciej, a niektóre z nich zmodyfikował, aby działały asynchronicznie. Wprowadziliśmy też strategię dotyczącą wydajności, dzięki której aplikacja może wstrzymać wykonywanie algorytmu i wziąć je w kolejną kolejkę, umożliwiając przeglądarce aktualizowanie interfejsu i uniknięcie utraty klatek. W przypadku aplikacji na iOS nie było to problemem, ponieważ może ona używać wątków i ocenić te algorytmy w tle, podczas gdy główny wątek iOS aktualizuje interfejs użytkownika.

Kolejnym problemem, który zespół inżynierów musiał rozwiązać, było przeniesienie interfejsu użytkownika opartego na elementach HTML dołączonych do DOM do interfejsu dokumentu opartego na pełnym ekranie. Na początku projektu wszystkie notatki i treści związane z dokumentem były wyświetlane w ramach struktury DOM za pomocą elementów HTML, tak jak w przypadku każdej innej strony internetowej. W pewnym momencie przełączyliśmy się na pełny ekran, aby poprawić wydajność na urządzeniach niskiej klasy poprzez skrócenie czasu, jaki przeglądarka potrzebuje na aktualizację DOM.

Zespół inżynierów uznał, że te zmiany mogłyby zmniejszyć liczbę problemów, gdyby zostały wprowadzone na początku projektu.

  • Odciążaj główny wątek, często używając instancji roboczych w przypadku algorytmów wymagających dużych zasobów.
  • Od samego początku używaj wyeksportowanychzaimportowanych funkcji zamiast biblioteki interoperacyjności JS-Swift, aby zmniejszyć wpływ na wydajność związany z wychodzeniem z kontekstu Wasm. Ta biblioteka JavaScript do interoperacyjności jest przydatna do uzyskiwania dostępu do DOM lub przeglądarki, ale działa wolniej niż natywne funkcje eksportowane przez Wasm.
  • Upewnij się, że kod umożliwia korzystanie z OffscreenCanvas w ramach aplikacji, aby mogła ona odciążyć główny wątek i przenieść wszystkie operacje interfejsu Canvas API do web workera, co zmaksymalizuje wydajność aplikacji podczas pisania notatek.
  • Przeniesienie wszystkich działań związanych z Wasm do instancji roboczej w przeglądarce lub nawet do puli takich instancji, aby aplikacja mogła zmniejszyć obciążenie głównego wątku.

Edytor tekstu

Innym interesującym problemem był związany z jednym konkretnym narzędziem, czyli edytorem tekstu. Implementacja tego narzędzia na iOS opiera się na NSAttributedString, małym zestawie narzędzi korzystającym z RTF. Ta implementacja nie jest jednak zgodna z SwiftWasm, więc zespół platformy wieloplatformowej musiał najpierw utworzyć niestandardowy parsujący na podstawie gramatyki RTF, a potem wdrożyć edycję, przekształcając RTF w HTML i odwrotnie. Tymczasem zespół iOS rozpoczął pracę nad nową implementacją tego narzędzia, która zastąpi użycie RTF modelem niestandardowym, aby aplikacja mogła wyświetlać tekst stylizowany w przyjazny sposób na wszystkich platformach używających tego samego kodu Swift.

Edytor tekstu Goodnotes.

To wyzwanie było jednym z najbardziej interesujących punktów w mapie drogowej projektu, ponieważ zostało rozwiązane w kroku po kroku na podstawie potrzeb użytkowników. Był to problem techniczny rozwiązany dzięki podejściu skoncentrowanym na użytkownikach. Zespół musiał przepisać część kodu, aby umożliwić renderowanie tekstu, a następnie dodać możliwość edycji tekstu w drugiej wersji.

Wersje iteracyjne

W ciągu ostatnich 2 lat projekt ewoluował w niesamowity sposób. Zespół zaczął pracować nad wersją projektu tylko do odczytu, a kilka miesięcy później udostępnił zupełnie nową wersję z wieloma możliwościami edycji. Aby często wdrażać zmiany kodu w wersji produkcyjnej, zespół postanowił stosować flagi funkcji. W przypadku każdej wersji zespół może włączyć nowe funkcje i opublikować zmiany kodu implementujące nowe funkcje, które użytkownik zobaczyłby dopiero po kilku tygodniach. Zespół uważa jednak, że można było coś zrobić lepiej. Uważają, że wprowadzenie dynamicznego systemu flag funkcji przyspieszyłoby proces, ponieważ wyeliminowałoby konieczność ponownego wdrażania w celu zmiany wartości flag. Dałoby to Goodnotes większą elastyczność i przyspieszyłoby wdrażanie nowej funkcji, ponieważ firma nie musiałaby łączyć wdrożenia projektu z wersją produktu.

Praca offline

Jedną z głównych funkcji, nad którą pracował zespół, jest obsługa offline. Możliwość edytowania i modyfikowania dokumentów to jedna z funkcji, których można oczekiwać od tego typu aplikacji. Nie jest to jednak prosta funkcja, ponieważ Goodnotes umożliwia współpracę. Oznacza to, że wszystkie zmiany wprowadzone przez różnych użytkowników na różnych urządzeniach powinny być widoczne na każdym urządzeniu bez konieczności rozwiązywania konfliktów przez użytkowników. Firma Goodnotes rozwiązała ten problem już dawno temu, używając karty CRD. Dzięki tym replikowanym typom danych bez konfliktów Goodnotes może łączyć wszystkie zmiany wprowadzone w dowolnym dokumencie przez dowolnego użytkownika i połączać je bez żadnych konfliktów. Korzystanie z IndexedDB i magazynu dostępnego dla przeglądarek internetowych było ogromnym ułatwieniem dla współpracy offline w internecie.

Aplikacja Goodnotes działa offline.

Dodatkowo otwarcie aplikacji internetowej Goodnotes powoduje początkowy koszt pobrania około 40 MB ze względu na rozmiar pliku binarnego Wasm. Początkowo zespół Goodnotes polegał wyłącznie na zwykłej pamięci podręcznej przeglądarki dla pakietu aplikacji i większości używanych punktów końcowych interfejsu API, ale z powodu niezawodności interfejsu Cache API i usług działających w tle zespół mógłby wcześniej skorzystać z tych rozwiązań. Zespół początkowo unikał tego zadania z powodu jego rzekomej złożoności, ale ostatecznie zdał sobie sprawę, że dzięki Workboxowi nie jest ono aż tak straszne.

Zalecenia dotyczące korzystania z Swifta w internecie

Jeśli masz aplikację na iOS z dużą ilością kodu, którego chcesz użyć ponownie, przygotuj się na niesamowitą podróż. Zanim zaczniesz, zapoznaj się z kilkoma przydatnymi wskazówkami.

  • Sprawdź, którego kodu chcesz użyć ponownie. Jeśli logika biznesowa Twojej aplikacji jest zaimplementowana po stronie serwera, prawdopodobnie zechcesz ponownie użyć kodu interfejsu użytkownika, a WebAssembly w tym przypadku Ci nie pomoże. Nasz zespół krótko przyjrzał się Tokamak, czyli frameworkowi zgodnemu z SwiftUI do tworzenia aplikacji przeglądarkowych za pomocą WebAssembly, ale nie był on wystarczająco dopracowany, aby spełniać wymagania aplikacji. Jeśli jednak Twoja aplikacja ma złożoną logikę biznesową lub algorytmy zaimplementowane w ramach kodu klienta, Wasm będzie Twoim najlepszym przyjacielem.
  • Upewnij się, że kod źródłowy Swift jest gotowy. Wzorce projektowania oprogramowania na potrzeby warstwy UI lub konkretne architektury, które zapewniają wyraźne oddzielenie logiki interfejsu od logiki biznesowej, będą bardzo przydatne, ponieważ nie będzie można ponownie użyć implementacji warstwy UI. Bardzo ważne będą też czysta architektura i zasady architektury heksagonalnej, ponieważ musisz wstrzyknąć i podać zależności dla całego kodu związanego z wejściem/wyjściem. Będzie to znacznie łatwiejsze, jeśli będziesz przestrzegać tych architektur, w których szczegóły implementacji są definiowane jako abstrakcje, a zasada odwrócenia zależności jest intensywnie wykorzystywana.
  • Wasm nie udostępnia kodu interfejsu. Dlatego zdecyduj, z jakiego frameworku interfejsu użytkownika chcesz korzystać w internecie.
  • JSKit pomoże Ci zintegrować kod Swift z JavaScriptem, ale pamiętaj, że jeśli masz ścieżkę szybkiego dostępu, przełączanie się między JavaScriptem a Swiftem może być kosztowne i będziesz musiał zastąpić je wyeksportowanymi funkcjami. Więcej informacji o tym, jak działa JSKit, znajdziesz w oficjalnej dokumentacji oraz w artykule Dynamic Member Lookup in Swift, a hidden gem!.
  • Możliwość ponownego użycia architektury zależy od architektury aplikacji i używanej biblioteki mechanizmu wykonywania kodu asynchronicznego. Wzory takie jak MVVP czy architektura składana pomogą Ci w ponownym użyciu modeli widoku i części logiki interfejsu użytkownika bez łączenia implementacji z zależnościami UIKit, których nie można używać z Wasm. RXSwift i inne biblioteki mogą nie być zgodne z Wasm, więc miej to na uwadze, ponieważ w kodzie Swift w Goodnotes musisz używać funkcji OpenCombine, async/await i streamów.
  • Skompresuj plik binarny Wasm za pomocą gzip lub brotli. Pamiętaj, że rozmiar pliku binarnego będzie dość duży w przypadku klasycznych aplikacji internetowych.
  • Nawet jeśli możesz używać Wasm bez PWA, pamiętaj, aby uwzględnić co najmniej service workera, nawet jeśli Twoja aplikacja internetowa nie ma pliku manifestu lub nie chcesz, aby użytkownik ją instalował. Usługa wtyczki zapisze i wyśle plik binarny Wasm bezpłatnie oraz wszystkie zasoby aplikacji, aby użytkownik nie musiał ich pobierać za każdym razem, gdy otwiera projekt.
  • Pamiętaj, że zatrudnianie może być trudniejsze niż się spodziewasz. Możesz potrzebować doświadczonych web developerów z nieco wiedzy o Swift lub doświadczonych deweloperów Swift z nieco wiedzy o web. Jeśli uda Ci się znaleźć inżynierów ogólnego przeznaczenia z pewną wiedzą na temat obu platform, byłoby to świetnie

Podsumowanie

Tworzenie projektu internetowego przy użyciu złożonego zestawu technologii podczas pracy nad produktem pełnym wyzwań to niesamowite doświadczenie. To będzie trudne, ale na pewno warto. Bez tego podejścia Goodnotes nie mogłaby wydać wersji na Windowsa, Androida, ChromeOS i internet, jednocześnie pracując nad nowymi funkcjami aplikacji na iOS. Dzięki temu rozwiązaniu technicznemu i zespołowi inżynierów Goodnotes jest teraz wszędzie, a zespół jest gotowy do podjęcia kolejnych wyzwań. Jeśli chcesz dowiedzieć się więcej o tym projekcie, obejrzyj wykład zespołu Goodnotes na konferencji NSSpain 2023. Wypróbuj Goodnotes na potrzeby przeglądarki internetowej.