どこでもグッドノート

iPad で製品を使用している女性が写った Goodnotes のマーケティング画像。

過去 2 年間、Goodnotes のエンジニアリング チームは、成功した iPad のメモ作成アプリを他のプラットフォームに導入するプロジェクトに取り組んできました。このケーススタディでは、2022 年の iPad アプリが、ウェブ テクノロジーを基盤とするウェブ、ChromeOS、Android、Windows に実装され、チームが 10 年以上にわたって取り組んできた同じ Swift コードを再利用した WebAssembly について説明します。

Goodnotes ロゴ。

ウェブ、Android、Windows に Goodnotes が登場した理由

2021 年、Goodnotes は iOS と iPad 向けのアプリとしてのみ提供されていました。Goodnotes のエンジニアリング チームは、Goodnotes の新しいバージョンを、追加のオペレーティング システムとプラットフォーム向けに作成するという大きな技術的課題を受け入れました。このプロダクトには完全な互換性があり、iOS アプリケーションと同じメモを表示する必要があります。PDF や添付される画像の上にメモを付けると、iOS アプリと同じストロークが表示される必要があります。追加するストロークは、ユーザーが使用していたツール(ペン、蛍光ペン、万年筆、図形、消しゴムなど)に関係なく、iOS ユーザーが作成できるものと同じである必要があります。

手書きのメモとスケッチが表示された Goodnotes アプリのプレビュー。

Swift コードベースは、すでに作成とテストが長年にわたって十分に行われており、要件とエンジニアリング チームの経験に基づいて、そのコードベースを再利用するのが最善であるとすぐに判断しました。しかし、既存の iOS/iPad アプリケーションを、Flutter や Compose マルチプラットフォームなどの別のプラットフォームやテクノロジーに移植しないのはなぜですか?新しいプラットフォームに移行するには Goodnotes を書き直す必要がありますこれを行うと、すでに実装されている iOS アプリケーションと、ゼロから構築される予定のアプリケーションとの間で開発競争が開始したり、新しいコードベースが追いつく間に既存のアプリケーションでの新規開発の停止を招いたりする可能性があります。Goodnotes が Swift コードを再利用できれば、クロスプラットフォーム チームが iOS チームが実装した新機能の恩恵を受けることができ、アプリの基礎と同等の機能の実現に取り組むことができます。

このプロダクトは、iOS で次のような機能を追加する際に生じる多くの興味深い課題をすでに解決していました。

  • メモのレンダリング。
  • ドキュメントとメモの同期。
  • 競合のない複製データ型を使用したメモの競合解決。
  • AI モデル評価のためのデータ分析。
  • コンテンツ検索とドキュメントのインデックス登録
  • カスタムのスクロール操作とアニメーション。
  • すべての UI レイヤのモデル実装を表示します。

これらはすべて、エンジニアリング チームが iOS および iPad アプリケーションで動作している iOS コードベースを入手し、Goodnotes が Windows、Android、またはウェブ アプリケーションとして出荷されるプロジェクトの一部として実行できれば、他のプラットフォームへの実装ははるかに容易になります。

Goodnotes の技術スタック

幸いなことに、ウェブで既存の Swift コードを再利用する方法がありました。それは、WebAssembly(Wasm)です。Goodnotes は、オープンソースおよびコミュニティで維持されているプロジェクト SwiftWasm で、Wasm を使用してプロトタイプを構築しました。Goodnotes チームは、SwiftWasm を使用して、すでに実装されているすべての Swift コードを使用して Wasm バイナリを生成できます。このバイナリは、Android、Windows、ChromeOS、その他すべてのオペレーティング システム用のプログレッシブ ウェブ アプリケーションとして出荷されるウェブページに含めることができます。

Goodnotes のロールアウト シーケンスは、Chrome から始まり、Windows、最後に Android と、Linux などの他のプラットフォームが PWA をベースにしています。

目的は、Goodnotes を PWA としてリリースし、すべてのプラットフォームのストアに掲載できるようにすることでした。iOS ですでに使用されているプログラミング言語である Swift と、ウェブ上で Swift コードを実行するために使用されている WebAssembly に加えて、このプロジェクトでは次のテクノロジーを使用しました。

  • TypeScript: ウェブ技術で最もよく使用されるプログラミング言語。
  • React と webpack: ウェブで最もよく利用されているフレームワークおよびバンドラ。
  • PWA と Service Worker: 他の iOS アプリと同様に動作するオフライン アプリケーションとしてアプリをリリースし、ストアまたはブラウザ自体からインストールできるため、このプロジェクトの大きなイネーブラーとなります。
  • PWABuilder: Goodnotes が PWA をネイティブの Windows バイナリにラップするために使用するメイン プロジェクト。これにより、チームは Microsoft Store からアプリを配布できるようになります。
  • Trusted Web Activity: PWA をネイティブ アプリとして内部的に配信するために会社が使用する最も重要な Android テクノロジー。

Swift、Wasm、React、PWA で構成される Goodnotes 技術スタック。

次の図は、従来の TypeScript と React を使用して実装したものと、SwiftWasm と標準 JavaScript、Swift、WebAssembly を使用して実装したものを示しています。プロジェクトのこの部分では、JSKit を使用します。これは Swift と WebAssembly 用の JavaScript 相互運用ライブラリで、必要に応じて Swift コードからエディタ画面の DOM を処理したり、ブラウザ固有の API を使用したりするためにチームが使用します。

モバイルとパソコンのアプリのスクリーンショット。Wasm によって駆動される特定の描画領域と、React によって駆動される UI 領域が示されています。

Wasm とウェブを使用する理由

Wasm は Apple によって正式にサポートされていませんが、Goodnotes のエンジニアリング チームはこのアプローチが最善の決定であると判断した理由は次のとおりです。

  • 10 万行を超えるコードの再利用
  • コアプロダクトの開発を継続しながら、クロス プラットフォーム アプリに貢献できること。
  • イテレーション開発プロセスにより、すべてのプラットフォームにできるだけ早くアクセスできる。
  • すべてのビジネス ロジックを複製することなく、同じドキュメントをレンダリングするように制御でき、実装に違いが生じる。
  • すべてのプラットフォームで同時に行われたすべてのパフォーマンス改善(およびすべてのプラットフォームで実装されたすべてのバグ修正)を活用する。

基本となるのは、10 万行を超えるコードと、レンダリング パイプラインを実装するビジネス ロジックの再利用でした。また、Swift コードと他のツールチェーンの互換性を確保することで、必要に応じて将来的に別のプラットフォームでこのコードを再利用できるようになります。

反復的なプロダクト開発

チームは、可能な限り迅速にユーザーに何かを提供するため、反復的なアプローチを採用しました。Goodnotes は、ユーザーが任意の共有ドキュメントを取得してどのプラットフォームからでも読める読み取り専用バージョンのプロダクトから始まりました。リンクを使用するだけで、iPad で書き込んだのと同じメモにアクセスして読むことができます。次のフェーズでは、クロス プラットフォーム版を iOS 版と同等にするため、編集機能を追加しました。

読み取り専用からフル機能のプロダクトへの移行を象徴する、2 つのアプリのスクリーンショット。

読み取り専用のプロダクトの最初のバージョンでは、開発に 6 か月を要しました。その後 9 か月は、最初の編集機能と UI 画面で、作成したドキュメントや他のユーザーが共有したドキュメントを確認できるものでした。さらに、SwiftWasm ツールチェーンのおかげで、iOS プラットフォームの新機能をクロス プラットフォーム プロジェクトに簡単に移植できました。たとえば、新しいタイプのペンを開発し、数千行のコードを再利用することで、クロス プラットフォームで容易に実装できるようにしました。

このプロジェクトの構築は素晴らしい経験で、Goodnotes はそこから多くのことを学びました。そのため、以降のセクションでは、ウェブ開発と WebAssembly や Swift などの言語の使用に関する興味深い技術ポイントに焦点を当てます。

最初の障害

このプロジェクトへの取り組みは、さまざまな観点から見て非常に困難でした。チームが最初に見つけた障害は、SwiftWasm ツールチェーンに関連していました。ツールチェーンはチームにとって大きなイネーブラーでしたが、すべての iOS コードが Wasm と互換性があるわけではありません。たとえば、ビューの実装、API クライアント、データベースへのアクセスなど、IO や UI に関連するコードは再利用できないため、チームはアプリの特定の部分をリファクタリングして、クロス プラットフォーム ソリューションから再利用できるようにする必要がありました。チームが作成した PR のほとんどは、依存関係を抽象化するためのリファクタリングであったため、チームは後で依存関係インジェクションやその他の同様の戦略を使用してこれらを置き換えることができました。iOS コードには元々、Wasm で実装できる未加工のビジネス ロジックと、入出力やユーザー インターフェースを担うコードとが混在していましたが、Wasm もサポートしていないために Wasm では実装できませんでした。そのため、Swift ビジネス ロジックをプラットフォーム間で再利用できるようになったら、IO コードと UI コードを TypeScript に再実装する必要がありました。

解決されたパフォーマンスの問題

Goodnotes がエディタの作業を開始すると、チームは編集機能に関するいくつかの問題を特定し、技術的な制約が厳しいことがロードマップに組み込まれました。最初の問題はパフォーマンスに関連していました。JavaScript はシングルスレッド言語です。つまり、1 つのコールスタックと 1 つのメモリヒープがあります。コードを順番に実行し、コードの実行を完了してから次のコードに進みます。これは同期的ですが、場合によっては有害なことがあります。たとえば、関数の実行に時間がかかる場合や何かを待機する必要がある場合、その間はすべてがフリーズします。エンジニアはこれを解決する必要がありましたレンダリング レイヤやその他の複雑なアルゴリズムに関連するコードベースの特定のパスを評価することは、これらのアルゴリズムが同期的であり、実行するとメインスレッドをブロックしていたため、チームにとって問題でした。Goodnotes チームは、それらを高速化するために書き直し、一部をリファクタリングして非同期にしました。また、アプリでアルゴリズムの実行を停止して後で続行できるように、ブラウザが UI を更新してフレームのドロップを回避できるように、ield 戦略を導入しました。iOS アプリでは、メインの iOS スレッドがユーザー インターフェースを更新している間に、バックグラウンドでスレッドを使用してこれらのアルゴリズムを評価できるため、これは問題ではありませんでした。

エンジニアリング チームが解決しなければならないもう一つのソリューションは、DOM に添付された HTML 要素に基づく UI を、全画面キャンバスに基づくドキュメント UI に移行することでした。このプロジェクトでは、他のウェブページと同様に HTML 要素を使用して、ドキュメントに関連するすべてのメモとコンテンツを DOM 構造の一部として表示し始めましたが、ある時点で全画面キャンバスに移行し、ブラウザで DOM の更新に要する時間を短縮することで、ローエンド デバイスでのパフォーマンスを向上させました。

エンジニアリング チームにより、プロジェクトの初めにこれらの変更を行っていれば、発生した問題の一部を軽減できる可能性があると特定されました。

  • 負荷の高いアルゴリズムではウェブ ワーカーを頻繁に使用して、メインスレッドのオフロードを増やす。
  • 当初から、JS-Swift 相互運用ライブラリではなく、エクスポート済み関数インポートされた関数を使用して、Wasm コンテキストから抜けた場合のパフォーマンスへの影響を軽減します。この JavaScript 相互運用ライブラリは、DOM またはブラウザへのアクセスに役立ちますが、ネイティブの Wasm エクスポート関数よりも低速です。
  • コードでメインスレッドをオフロードし、Canvas API のすべての使用をウェブワーカーに移行できるように、内部で OffscreenCanvas の使用が許可されていることを確認します。これにより、メモ作成時のアプリケーションのパフォーマンスを最大化できます。
  • Wasm 関連のすべての実行をウェブワーカーまたはウェブワーカーのプールに移動して、アプリのメインスレッド ワークロードを軽減できるようにします。

テキスト エディタ

もう 1 つの興味深い問題は、テキスト エディタという特定のツールに関連することでした。このツールの iOS 実装は NSAttributedString に基づいています。これは、内部で RTF を使用する小さなツールセットです。ただし、この実装は SwiftWasm と互換性がないため、クロスプラットフォーム チームはまず RTF 文法に基づくカスタム パーサーを作成し、その後で RTF を HTML に変換して編集機能を実装する必要がありました(その逆も同様)。一方、iOS チームは、RTF の使用をカスタムモデルに置き換えて、このツールの新しい実装への取り組みを開始しました。これにより、同じ Swift コードを共有するすべてのプラットフォームで、スタイル付きテキストをわかりやすい方法で表示できるようになります。

Goodnotes テキスト エディタ。

この課題は、ユーザーのニーズに基づいて繰り返し解決されたため、プロジェクト ロードマップの最も興味深いポイントの一つでした。これは、ユーザー重視のアプローチを使用して解決されたエンジニアリング上の問題であり、2 番目のリリースでテキスト編集を可能にするために、テキストをレンダリングできるようにコードの一部を書き換える必要がありました。

反復リリース

この 2 年間で、このプロジェクトは驚異的な進化を遂げました。チームはプロジェクトの読み取り専用バージョンの開発に着手し、数か月後に多くの編集機能を備えた新しいバージョンを出荷しました。コードの変更を本番環境に頻繁にリリースするため、チームは機能フラグを幅広く使用することにしました。チームは、リリースごとに新機能を有効にしたり、数週間後にユーザーに表示される新機能を実装したコード変更をリリースしたりしました。ただし、改善の余地があると思われる部分はあります。動的フィーチャー トグル システムを導入すると、フラグ値を変更するために再デプロイの必要がなくなるため、スピードアップに役立つと考えています。これにより、Goodnotes は柔軟性が高まり、新機能のデプロイが迅速化されます。これは、Goodnotes ではプロジェクトのデプロイをプロダクト リリースにリンクする必要がないためです。

オフライン作業

チームが取り組んだ主な機能の 1 つはオフライン サポートです。ドキュメントの編集と修正が可能な機能は、このようなアプリケーションに期待できる機能の 1 つです。ただし、Goodnotes はコラボレーションをサポートしているため、これは単純な機能ではありません。つまり、異なるデバイスで異なるユーザーが行ったすべての変更が、競合の解決をユーザーに求めることなく、すべてのデバイスに適用される必要があります。Goodnotes は、内部で CRDT を使用して、ずっと前にこの問題を解決しました。競合のない複製データ型のおかげで、Goodnotes はどのユーザーがどのドキュメントに対しても行ったすべての変更を結合し、マージの競合を発生させずに変更をマージできます。IndexedDB とウェブブラウザで利用可能なストレージの使用は、ウェブ上での共同作業におけるオフライン エクスペリエンスの大きな成功要因となりました。

オフラインでも動作する Goodnotes アプリ。

さらに、Goodnotes ウェブアプリを開くと、Wasm のバイナリサイズにより、初回の初期ダウンロード コストは約 40 MB になります。当初、Goodnotes チームは、App Bundle 自体と使用するほとんどの API エンドポイントについて、通常のブラウザ キャッシュのみに依存していましたが、後で考えると、以前より信頼性の高い Cache API と Service Worker から利益を得ることができました。当初は複雑さが想定されるため、このタスクを回避しようとしていましたが、最終的には、Workbox によって攻撃がはるかに軽減されたことに気づきました。

ウェブで Swift を使用する際の推奨事項

再利用したいコードが多い iOS アプリの場合は、素晴らしい取り組みに着手しようとしています。始める前に、いくつか興味深いヒントがあります。

  • 再利用するコードのチェックボックスをオンにします。アプリのビジネス ロジックがサーバー側で実装されている場合は、UI コードを再利用したいと思う可能性が高いですが、ここでは Wasm の支援はありません。チームは、WebAssembly を使用してブラウザアプリを構築するための SwiftUI 互換フレームワークである Tokamak を簡単に検討しましたが、アプリのニーズを満たすには不十分でした。ただし、アプリに強力なビジネス ロジックやアルゴリズムがクライアント コードの一部として実装されている場合は、Wasm の使用をおすすめします。
  • Swift コードベースの準備が整っていることを確認します。UI レイヤまたは UI ロジックとビジネス ロジックを厳密に分離する、UI レイヤまたは特定のアーキテクチャのソフトウェア設計パターンは、UI レイヤの実装を再利用できないため、非常に便利です。すべての IO 関連コードに依存関係を挿入して提供する必要があり、実装の詳細が抽象化として定義され、依存関係の逆転の原則が頻繁に使用されるこれらのアーキテクチャに従うと、クリーンなアーキテクチャまたは六角形のアーキテクチャの原則も基本的なものになります。
  • Wasm は UI コードを提供していません。ウェブに使用する UI フレームワークを 決定してください
  • JSKit は、Swift コードを JavaScript と統合するのに役立ちますが、ホットパスがある場合、JS-Swift ブリッジをまたぐのはコストがかかる場合があり、エクスポートされた関数に置き換える必要があります。JSKit の内部の仕組みについて詳しくは、公式ドキュメントSwift の「Dynamic Member Lookup」(隠れた名詞)の投稿をご覧ください。
  • アーキテクチャを再利用できるかどうかは、アプリが従うアーキテクチャと、使用する非同期コード実行メカニズム ライブラリによって異なります。MVVP やコンポーザブル アーキテクチャなどのパターンを使用すると、実装を Wasm では使用できない UIKit 依存関係に結合することなく、ビューモデルや UI ロジックの一部を再利用できます。RXSwift などのライブラリは Wasm と互換性がない可能性があるため、Goodnotes の Swift コードで OpenCombine、async/await、streams を使用する必要があるため、その点に留意してください。
  • gzip または brotli を使用して Wasm バイナリを圧縮します。従来のウェブ アプリケーションでは、バイナリのサイズが非常に大きいことに留意してください。
  • PWA なしで Wasm を使用できる場合でも、ウェブアプリにマニフェストがない場合や、ユーザーにインストールさせたくない場合でも、少なくとも Service Worker を含めるようにしてください。Service Worker は、無料で Wasm バイナリとすべてのアプリリソースを保存して提供するため、ユーザーはプロジェクトを開くたびにリソースをダウンロードする必要はありません。
  • 採用は予想以上に難しくなる場合があります。Swift の使用経験がある優れたウェブ デベロッパーや、ウェブにある程度の経験がある Swift デベロッパーの採用が必要になることもあります。両方のプラットフォームで一定の知識を持つ ジェネラリストエンジニアがいれば

まとめ

さまざまな課題に直面したプロダクトに取り組みながら、複雑な技術スタックを使用してウェブ プロジェクトを構築するのは素晴らしい経験です。難しいけど それだけの価値があるこのアプローチなしに、iOS アプリの新機能に取り組むにあたり、Goodnotes は Windows、Android、ChromeOS、ウェブ向けのバージョンをリリースすることはできませんでした。この技術スタックと Goodnotes のエンジニアリング チームのおかげで、Goodnotes は世界中どこでも利用できるようになり、チームは次の課題に取り組む準備が整いました。このプロジェクトについて詳しくは、Goodnotes チームが NS Spanish 2023 で行った講演をご覧ください。Goodnotes for web をぜひお試しください。