Notatki w każdym miejscu

Obraz marketingowy firmy Goodnotes, na którym widać kobietę korzystającą z produktu na iPadzie.

Przez ostatnie 2 lata zespół inżynierów Goodnotes pracował nad projektem, który ma na celu wprowadzenie popularnej aplikacji do notatek na iPadach na inne platformy. To studium przypadku pokazuje, w jaki sposób aplikacja roku na iPada z 2022 r. została udostępniona w internecie oraz na urządzeniach z ChromeOS, Android i Windows opartych na technologiach internetowych, a WebAssembly wykorzystuje ten sam kod Swift, nad którym zespół pracuje od ponad 10 lat.

Logo Goodnotes.

Dlaczego Goodnotes pojawi się w internecie, na Androidzie i w Windowsie

W 2021 roku aplikacja Goodnotes była dostępna tylko na iOS i iPada. Zespół inżynierów w firmie Goodnotes podjął ogromne wyzwanie techniczne: stworzenie nowej wersji Goodnotesa, ale na potrzeby dodatkowych systemów operacyjnych i platform. Usługa powinna być w pełni zgodna i renderować te same notatki co aplikacja na iOS. Wszelkie notatki wykonane na górze pliku PDF lub dołączone do niego zdjęcia powinny być równoważne i zawierać te same kreski, które pokazuje aplikacja na iOS. Każda dodana kreska powinna być taka sama jak ta, którą może utworzyć użytkownik iOS, niezależnie od użytego narzędzia, np. pióra, zakreślacza, pióra wiecznego, kształtów czy gumki.

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

Na podstawie wymagań i doświadczenia zespołu inżynierów szybko stwierdziliśmy, że najlepszym rozwiązaniem będzie ponowne użycie bazy kodu Swift, ponieważ została ona już napisana i dokładnie przetestowana przez wiele lat. Może lepiej przenieść dotychczasową aplikację na iOS lub iPada na inną platformę lub technologię, np. Flutter lub Compose Multiplatform? Przejście na nową platformę wymagałoby przeredagowania pliku Goodnotes. W ten sposób możesz rozpocząć wyścig między już wdrożoną aplikacją na iOS a utworzoną od zera z zerową aplikacją lub przerwać tworzenie nowych wersji istniejących aplikacji w czasie, gdy nowa baza kodu będzie nad nimi pracować. Gdyby firma Goodnotes mogła ponownie użyć kodu Swift, zespół ds. iOS mógł skorzystać na nowych funkcjach wdrożonych przez zespół ds. iOS, a zespół pracujący nad różnymi platformami pracował nad podstawami aplikacji i spójnością funkcji związanych z zasięgiem.

Rozwiązano już wiele ciekawych problemów z iOS związanych z dodaniem takich funkcji jak:

  • Renderowanie notatek.
  • Synchronizacja dokumentów i notatek.
  • Rozwiązywanie konfliktów w notatkach korzystających z typów danych zreplikowanych bez konfliktów.
  • Analiza danych na potrzeby oceny modelu AI.
  • Wyszukiwanie treści i indeksowanie dokumentów.
  • Niestandardowe przewijanie i animacje.
  • Zobacz implementację modelu dla wszystkich warstw interfejsu użytkownika.

Wszystkie z nich byłoby dużo łatwiejsze do wdrożenia na innych platformach, gdyby zespół inżynierów mógł wdrożyć bazę kodu na iOS działającą dla aplikacji na iOS i iPada i uruchomić ją w ramach projektu, który Goodnotes może być dostarczany jako aplikacje na Windows, Androida lub aplikacje internetowe.

Stos technologiczny Goodnotes

Na szczęście można było wykorzystać istniejący kod Swift w internecie – WebAssembly (Wasm). Firma Goodnotes zbudowała prototyp przy użyciu Wasm w ramach projektu open source i zarządzanego przez społeczność projektu SwiftWasm. Dzięki SwiftWasm zespół Goodnotes może wygenerować plik binarny Wasm przy użyciu całego już zaimplementowanego kodu Swift. Ten plik binarny może być umieszczony na stronie internetowej wysyłanej jako progresywna aplikacja internetowa na Androida, Windowsa, ChromeOS i pozostałe systemy operacyjne.

Sekwencja wdrażania w Goodnotes rozpoczynająca się od Chrome przez Windows, Androida i inne platformy takie jak Linux na końcu – wszystko na podstawie PWA.

Celem było opublikowanie aplikacji Goodnotes jako aplikacji PWA, tak aby można było umieścić ją w sklepie na każdej platformie. Oprócz języka Swift (języka programowania używanego już w iOS) oraz WebAssembly używanego do wykonywania kodu Swift w internecie projekt wykorzystywał następujące technologie:

  • TypeScript: najczęściej używany język programowania w technologiach internetowych.
  • React i webpack: najpopularniejsza platforma i pakiet narzędzi do tworzenia pakietów w internecie.
  • PWA i mechanizmy service worker: ten projekt ma duże znaczenie, ponieważ nasz zespół może wysłać naszą aplikację w postaci aplikacji offline, która działa jak każda inna aplikacja na iOS, a Ty możesz ją zainstalować ze sklepu lub w przeglądarce.
  • PWABuilder: główny projekt w Goodnotes służący do spakowania PWA do natywnego pliku binarnego systemu Windows, tak by zespół mógł rozpowszechniać aplikację ze sklepu Microsoft Store.
  • Trusted Web Activities: najważniejsza technologia Androida używana przez firmę do rozpowszechniania naszej aplikacji PWA jako aplikacji natywnej.

Stos technologiczny Goodnotes składający się z Swift, Wasm, React i PWA.

Na rysunku poniżej widać, co jest zaimplementowane za pomocą klasycznej wersji TypeScriptu i React oraz tego, co zostało wdrożone za pomocą kodu SwiftWasm i vanilla JavaScript, Swift i WebAssembly. W tej części projektu wykorzystamy JSKit – bibliotekę interoperacyjności JavaScript dla Swift i WebAssembly, której zespół używa do obsługi DOM na ekranie edytora z kodu Swift, a nawet używa niektórych interfejsów API związanych z przeglądarkami.

Zrzuty ekranu aplikacji na urządzenia mobilne i komputery przedstawiające konkretne obszary rysowania rysowane przez Wasm oraz obszary interfejsu generowane przez React.

Dlaczego warto korzystać z Wasm i internetu?

Chociaż firma Wasm nie jest oficjalnie obsługiwana przez firmę Apple, zespół inżynierów Goodnotes uznał, że to podejście było najlepszą decyzją:

  • Ponowne wykorzystanie ponad 100 tys. wierszy kodu.
  • Możliwość dalszego tworzenia głównej usługi, a jednocześnie pomagania w tworzeniu aplikacji działających na wielu platformach.
  • Możliwości jak najszybszego dostępu do każdej platformy za pomocą iteracyjnego procesu programowania.
  • Możliwość kontrolowania wyświetlania tego samego dokumentu bez duplikowania całej logiki biznesowej i wprowadzania różnic w implementacjach.
  • Korzystanie ze wszystkich ulepszeń w zakresie wydajności wprowadzonych w tym samym czasie na każdej platformie (oraz ze wszystkich poprawek błędów wdrożonych na każdej z nich).

Najważniejsze było ponowne wykorzystanie ponad 100 tys. wierszy kodu oraz logikę biznesową wdrażającą nasz potok renderowania. Jednocześnie zapewnienie zgodności kodu Swift z innymi łańcuchami narzędzi pozwala w razie potrzeby wykorzystać go na innych platformach.

iteracyjne rozwijanie usług

Zespół zastosował iteracyjne podejście, aby jak najszybciej udostępnić użytkownikom odpowiednie rozwiązania. Na początku w Goodnotes można było korzystać z wersji tylko do odczytu, która umożliwia użytkownikom pobieranie dowolnych udostępnionych dokumentów i odczytywanie ich na dowolnej platformie. Mając link, będą mogli otworzyć i przeczytać te same notatki, które stworzyli na iPadzie. W kolejnym etapie dodaliśmy funkcje edycji, aby wersje na wielu platformach były równoważne z wersji na iOS.

Dwa zrzuty ekranu z aplikacji symbolizujące przejście z trybu tylko do odczytu do pełnej wersji produktu.

Opracowanie pierwszej wersji usługi zajęło 6 miesięcy, a kolejne 9 miesięcy poświęciliśmy na wprowadzenie kilku funkcji edycyjnych oraz na ekran interfejsu, na którym możesz sprawdzić wszystkie utworzone przez siebie dokumenty lub udostępnione Ci dokumenty. Ponadto dzięki łańcuchowi narzędzi SwiftWasm nowe funkcje platformy iOS można łatwo przenieść do projektu na wielu platformach. Na przykład opracowano nowy typ pióra, który można łatwo wdrożyć na wielu platformach przez ponowne wykorzystanie tysięcy wierszy kodu.

Stworzenie tego projektu było niezwykłym doświadczeniem, a firma Goodnotes wiele się z niej nauczyła. Dlatego w kolejnych częściach skupimy się na interesujących zagadnieniach technicznych związanych z tworzeniem stron internetowych, używaniem WebAssembly i języków takich jak Swift.

Początkowe przeszkody

Praca nad tym projektem była bardzo wyzwaniem z wielu różnych punktów widzenia. Pierwszą przeszkodą, którą zespół napotkał, była przeszkoda związana z łańcuchem narzędzi SwiftWasm. Łańcuch narzędzi był świetnym narzędziem ułatwiającym pracę zespołu, ale nie cały kod na iOS był zgodny z Wasm. Na przykład kodu powiązanego z zamówieniem reklamowym lub interfejsem użytkownika (np. implementacją widoków danych, klientów interfejsu API czy dostępem do bazy danych) nie dało się użyć, więc zespół musiał zacząć refaktoryzować określone części aplikacji, aby móc je wykorzystać w rozwiązaniu wieloplatformowym. Większość utworzonych przez zespół informacji PR zawierało refaktoryzację abstrakcyjnych zależności, więc zespół mógł je później zastąpić za pomocą wstrzykiwania zależności lub innych podobnych strategii. Kod na iOS łączył początkowo nieprzetworzoną logikę biznesową z kodem odpowiedzialnym za wejścia i wyjścia oraz interfejs użytkownika, którego nie można zaimplementować w Wasm, ponieważ Wasm go nie obsługuje. Gdy logika biznesowa Swift była gotowa do ponownego użycia na różnych platformach, trzeba było ponownie zaimplementować logikę biznesową w języku Swift.

Rozwiązano problemy ze skutecznością

Gdy firma Goodnotes zaczęła pracować nad edytorem, zespół stwierdził, że ma problemy z edycją, a nasze plany wzięły pod uwagę ograniczenia techniczne. Pierwszy problem dotyczył skuteczności. JavaScript jest językiem jednowątkowym. Oznacza to, że ma 1 stos wywołań i 1 stertę pamięci. Wykonuje kod w podanej kolejności i musi zakończyć wykonywanie fragmentu kodu, zanim przejdzie do następnego. Jest synchroniczna, ale czasami może być szkodliwa. Jeśli na przykład wykonanie funkcji trochę trwa lub wymaga oczekiwania, w międzyczasie wszystko zostaje zablokowane. To właśnie ten problem musieli rozwiązać. Ocenianie pewnych ścieżek w bazie kodu związanych z warstwą renderowania lub innymi złożonymi algorytmami stanowiło problem dla naszego zespołu, ponieważ były one synchroniczne, a ich wykonywanie powodowało zablokowanie głównego wątku. Zespół Goodnotes opracował je w taki sposób, aby były szybsze, i refaktoryzował niektóre z nich, aby były asynchroniczne. Wprowadzono też strategię zysku, dzięki której aplikacja może zatrzymać wykonanie algorytmu i kontynuować go w późniejszym czasie, pozwalając przeglądarce na aktualizowanie interfejsu i unikanie pomijania ramek. Nie stanowiło to problemu w przypadku aplikacji na iOS, ponieważ może ona używać wątków i oceniać te algorytmy w tle, podczas gdy główny wątek iOS aktualizuje interfejs użytkownika.

Kolejnym rozwiązaniem, które zespół inżynierów musiał rozwiązać, było przeniesienie interfejsu opartego na elementach HTML dołączonych do modelu DOM do interfejsu dokumentu opartego na kanwie pełnoekranowej. Projekt zaczął wyświetlać wszystkie notatki i treści związane z dokumentem w ramach struktury DOM przy użyciu elementów HTML, tak jak miałoby to miejsce w przypadku każdej innej strony internetowej. Jednak w jakimś momencie został przeniesiony do pełnoekranowej przestrzeni roboczej, aby poprawić wydajność na słabszych urządzeniach, skracając czas poświęcany przez przeglądarkę na aktualizacje DOM.

Zespół inżynierów stwierdził, że poniższe zmiany mogłyby zmniejszyć liczbę napotkanych problemów, jeśli nie wprowadziły ich na początku projektu.

  • Większe odciążenie wątku głównego przez częste używanie instancji roboczych do obsługi skomplikowanych algorytmów.
  • Od samego początku korzystaj z wyeksportowanych i zaimportowanych funkcji zamiast biblioteki interoperacyjności JS-Swift, aby zmniejszyć wpływ na wydajność wyjściową z kontekstu Wasm. Ta biblioteka interoperacyjności JavaScript ułatwia dostęp do DOM lub przeglądarki, ale jest wolniejsza niż natywne funkcje eksportowane przez Wasm.
  • Upewnij się, że kod pozwala na zaawansowane korzystanie z interfejsu OffscreenCanvas, aby aplikacja mogła przenieść z niego cały wątek główny i przenieść całe wykorzystanie interfejsu Canvas API do instancji roboczej, maksymalizując wydajność aplikacji podczas pisania notatek.
  • Przenieś wszystkie wykonania związane z Wasm do instancji roboczej lub nawet puli instancji roboczych, aby aplikacja mogła zmniejszyć obciążenie głównego wątku.

Edytor tekstu

Kolejnym interesującym problemem był jeden z konkretnych narzędzi – edytor tekstu. Implementacja tego narzędzia w systemie iOS opiera się na NSAttributedString – małym zbiorze narzędzi wykorzystującym standard RTF. Ta implementacja nie jest jednak zgodna ze SwiftWasm, dlatego zespół działający na wielu platformach musiał najpierw utworzyć niestandardowy parser na podstawie gramatyki RTF, a później wdrożyć proces edycji, przekształcając pliki RTF w HTML i odwrotnie. W tym czasie zespół ds. iOS zaczął pracować nad nową implementacją tego narzędzia. Zastąpił on użycie RTF modelem niestandardowym, dzięki czemu aplikacja może prezentować tekst ze stylem w przyjazny sposób na wszystkich platformach korzystających z tego samego kodu Swift.

Edytor tekstu Goodnotes.

Wyzwanie to było jednym z najciekawszych punktów w planie projektu, ponieważ rozwiązano go iteracyjnie, z uwzględnieniem potrzeb użytkowników. Był to problem techniczny, który rozwiązano przy użyciu podejścia ukierunkowanego na użytkownika, w którym zespół musiał przeredagować część kodu, aby móc renderować tekst, a w drugiej wersji wprowadzić możliwość edytowania tekstu.

Wersje iteracyjne

Rozwój projektu w ciągu ostatnich dwóch lat był niezwykły. Zespół zaczął pracę nad wersją projektu tylko do odczytu, a miesiące później dostarczył zupełnie nową wersję z mnóstwem możliwości edycji. Aby często wprowadzać zmiany w kodzie w środowisku produkcyjnym, zespół zdecydował się na intensywne używanie flag funkcji. W przypadku każdej wersji zespół może włączać nowe funkcje, a także wprowadzać zmiany w kodzie, wdrażające nowe funkcje, które użytkownik widzi po kilku tygodniach. Nasz zespół uważa jednak, że można go poprawić. Uważa, że wprowadzenie systemu dynamicznych flag funkcji pomogłoby przyspieszyć cały proces, ponieważ eliminowałoby potrzebę ponownego wdrożenia w celu zmiany wartości flag. Zapewni to większą elastyczność i przyspieszy wdrażanie nowej funkcji, ponieważ w aplikacji Goodnotes nie trzeba będzie łączyć wdrożenia w ramach projektu z jej wersją.

Praca offline

Jedną z głównych funkcji, nad którą pracował zespół, była obsługa offline. Możliwość edytowania i modyfikowania dokumentów to jedna z funkcji, których można oczekiwać od każdej aplikacji tego typu. Nie jest to jednak prosta funkcja, ponieważ Goodnote obsługuje współpracę. Oznacza to, że wszystkie zmiany wprowadzone przez różnych użytkowników na różnych urządzeniach powinny trafiać na każde urządzenie bez potrzeby proszenia użytkowników o rozwiązywanie konfliktów. Aplikacja Goodnotes rozwiązała ten problem dawno temu, korzystając z dostępnych w standardzie CRDT. Dzięki typom powielonym danych niewymagającym konfliktów, Goodnotes może połączyć wszystkie zmiany wprowadzone w dokumencie przez dowolnego użytkownika i połączyć je bez konfliktu scalania. Wykorzystanie IndexedDB i dostępnego miejsca na dane dla przeglądarek internetowych znacznie ułatwią współpracę w internecie w trybie offline.

Aplikacja Goodnotes działa w trybie offline.

Poza tym otwarcie aplikacji internetowej Goodnotes wiąże się z początkowym kosztem pobierania około 40 MB ze względu na rozmiar pliku binarnego Wasm. Początkowo zespół Goodnotes korzystał wyłącznie z standardowej pamięci podręcznej przeglądarki dla pakietu aplikacji i większości używanych punktów końcowych API, ale z czasem z czasem mogło korzystać z bardziej niezawodnego interfejsu Cache API i skryptów service worker. Początkowo zespół zaniechał tego zadania ze względu na jego złożoność, ale w końcu zauważyło, że dzięki Workbox o wiele mniej się przestraszyło.

Zalecenia dotyczące korzystania z Swift w przeglądarce

Jeśli masz aplikację na iOS z dużą ilością kodu, którą chcesz wykorzystać ponownie, przygotuj się, ponieważ rozpocznie się niesamowita podróż. Jest kilka wskazówek, które mogą Cię zainteresować, zanim zaczniesz.

  • Zaznacz kod, którego chcesz użyć ponownie. Jeśli logika biznesowa Twojej aplikacji jest zaimplementowana po stronie serwera, prawdopodobnie zechcesz ponownie użyć kodu jej interfejsu, a Wasm nie będzie w tym miejscu pomóc. Zespół przez chwilę zajął się Tokamak – platformą zgodną z SwiftUI do tworzenia aplikacji do przeglądania internetu za pomocą WebAssembly, ale ta platforma nie była wystarczająco dojrzała. Jeśli jednak Twoja aplikacja ma solidną logikę biznesową lub algorytmy wdrożone w ramach kodu klienta, Wasm będzie Twoim najlepszym przyjacielem.
  • Sprawdź, czy baza kodu Swift jest gotowa. Wzorce projektowania oprogramowania dla warstwy UI lub określonych architektur tworzące silne odseparowanie logiki UI od logiki biznesowej będą bardzo przydatne, ponieważ nie będzie można ponownie użyć implementacji warstwy UI. Podstawowe będą również czysta architektura lub zasady architektury sześciokątnej, ponieważ trzeba wstrzyknąć i określić zależności dla całego kodu związanego z zamówieniem reklamowym. Będzie to znacznie łatwiejsze, jeśli będziesz postępować zgodnie z tymi architekturami, w których szczegóły implementacji są zdefiniowane jako abstrakcje, a w dużym stopniu stosowana jest zasada odwrócenia zależności.
  • Wasm nie udostępnia kodu interfejsu. Zdecyduj, której struktury interfejsu chcesz używać w sieci.
  • JSKit pomoże Ci zintegrować kod Swift z JavaScriptem. Pamiętaj jednak, że jeśli masz ścieżkę typu hotpath, przejście przez most JS-Swift może być kosztowne i trzeba będzie zastąpić go wyeksportowanymi funkcjami. Więcej o JSKit dowiesz się z oficjalnej dokumentacji oraz z posta Dynamiczne wyszukiwanie członków w Swift – ukryty klejnot!.
  • Możliwość ponownego użycia architektury będzie zależeć od architektury, z której korzysta Twoja aplikacja, oraz od używanej biblioteki mechanizmów wykonywania kodu asynchronicznego. Wzorce takie jak MVP lub architektura kompozycyjna pomogą Ci ponownie wykorzystywać modele widoku i część logiki UI bez łączenia implementacji z zależnościami UIKit, których nie można używać z Wasm. Biblioteki RXSwift i inne biblioteki mogą być niezgodne z Wasm, dlatego pamiętaj o tym, ponieważ w kodzie Swift trzeba używać OpenCombine, asynchronicznego/wstępnego oraz strumieni.
  • Skompresuj plik binarny Wasm za pomocą narzędzia gzip lub brotli. Pamiętaj, że rozmiar pliku binarnego będzie duży dla klasycznych aplikacji internetowych.
  • Nawet jeśli możesz używać Wasm bez PWA, pamiętaj o dodaniu skryptu service worker nawet wtedy, gdy aplikacja internetowa nie ma pliku manifestu lub nie chcesz, aby użytkownik ją instalował. Skrypt service worker zapisze i udostępni plik binarny Wasm bezpłatnie i zapewni wszystkie zasoby aplikacji, dzięki czemu użytkownik nie będzie musiał ich pobierać za każdym razem, gdy otwiera projekt.
  • Pamiętaj, że zatrudnienie może być trudniejsze, niż się spodziewasz. Konieczne może być zatrudnienie doświadczonych programistów z doświadczeniem w dziedzinie Swift lub takich z doświadczeniem w korzystaniu z internetu. Byłoby wspaniale, gdyby można było znaleźć programistów mających pewną wiedzę na temat obu platform,

Podsumowanie

Realizacja projektu internetowego za pomocą złożonego stosu technologicznego podczas pracy nad usługą pełną wyzwań to niesamowite doświadczenie. To będzie trudne, ale naprawdę warto. Bez tego podejścia firma Goodnotes nie mogła opublikować wersji na systemy Windows, Android, ChromeOS ani web w ramach prac nad nowymi funkcjami aplikacji na iOS. Dzięki stosowi technologicznemu i zespole inżynierów Goodnotes jest on teraz wszędzie dostępny, a zespół jest gotowy do kontynuowania pracy nad kolejnymi wyzwaniami. Jeśli chcesz dowiedzieć się więcej o tym projekcie, obejrzyj rozmowę z zespołem Goodnotes, która odbyła się na NSSpain 2023. Wypróbuj Goodnotes w internecie.