どこでもグッドノート

女性が iPad でアプリを使用している様子を写した Goodnotes のマーケティング画像。

過去 2 年間、Goodnotes エンジニアリング チームは、成功を収めた iPad のメモアプリを他のプラットフォームにも展開するプロジェクトに取り組んできました。このケーススタディでは、2022 年の iPad アプリ オブザイヤーが、ウェブ技術と WebAssembly を活用して、チームが 10 年以上にわたって取り組んできた同じ Swift コードを再利用して、ウェブ、ChromeOS、Android、Windows に移植された方法について説明します。

Goodnotes のロゴ。

Goodnotes がウェブ、Android、Windows に対応した理由

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

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

要件とエンジニアリング チームの経験に基づいて、Swift のコードベースはすでに作成され、長年にわたって十分にテストされているため、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 を使用したプロトタイプを構築しました。SwiftWasm により、Goodnotes チームは、すでに実装されているすべての Swift コードを使用して Wasm バイナリを生成できました。このバイナリは、Android、Windows、ChromeOS、その他のオペレーティング システム向けのプログレッシブ ウェブアプリとして配布されるウェブページに含めることができます。

Goodnotes のロールアウト順序は、Chrome、Windows、Android、最後に Linux などの他のプラットフォームで、すべて PWA に基づいています。

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

  • TypeScript: ウェブ技術で最もよく使用されるプログラミング言語。
  • React と webpack: ウェブで最も人気のあるフレームワークとバンドルツール。
  • PWA とサービス ワーカー: このプロジェクトを大きく前進させる要素です。他の iOS アプリと同様に動作するオフライン アプリとしてアプリをリリースでき、ストアまたはブラウザ自体からインストールできるためです。
  • PWABuilder: Goodnotes が PWA をネイティブ Windows バイナリにラップするために使用するメイン プロジェクト。これにより、チームは Microsoft Store からアプリを配布できます。
  • Trusted Web Activities: ネイティブ アプリとして PWA を配布するために Google が使用する最も重要な 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 を更新し、フレームのドロップを回避できます。iOS アプリケーションでは、メインの iOS スレッドがユーザー インターフェースを更新する間、スレッドを使用してこれらのアルゴリズムをバックグラウンドで評価できるため、これは問題ではありませんでした。

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

エンジニアリング チームは、プロジェクトの開始時に以下の変更を行っていれば、発生した問題の一部を軽減できた可能性があると判断しています。

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

テキスト エディタ

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

Goodnotes のテキスト エディタ。

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

反復リリース

この 2 年間で、このプロジェクトは驚くほど進化しました。チームはプロジェクトの読み取り専用バージョンの開発を開始し、数か月後に、多くの編集機能を備えたまったく新しいバージョンをリリースしました。コード変更を本番環境に頻繁にリリースするため、チームは特徴フラグを広範に使用することにしました。チームはリリースごとに新機能を有効にし、ユーザーが数週間後に目にする新機能を実装するコード変更をリリースすることもできます。ただし、チームは改善すべき点があったと考えています。動的機能フラグ システムを導入すると、フラグ値を変更するために再デプロイする必要がなくなるため、作業をスピードアップできると考えています。これにより、Goodnotes の柔軟性が向上し、プロジェクトのデプロイをプロダクト リリースにリンクする必要がないため、新機能のデプロイも迅速になります。

オフライン作業

チームが取り組んだ主な機能の一つがオフライン サポートです。ドキュメントの編集や変更は、このようなアプリに期待される機能の 1 つです。ただし、Goodnotes はコラボレーションをサポートしているため、これは単純な機能ではありません。つまり、異なるユーザーが異なるデバイスで行ったすべての変更が、ユーザーに競合の解決を求めることなく、すべてのデバイスに反映される必要があります。Goodnotes では、内部で CRDT を使用して、この問題を長い間解決してきました。これらの競合のない複製データ型により、Goodnotes では、任意のユーザーが任意のドキュメントに対して行ったすべての変更を組み合わせて、マージの競合なしで変更を統合できます。IndexedDB とウェブブラウザで利用可能なストレージの使用は、ウェブでのコラボレーション オフライン エクスペリエンスの大きな推進力でした。

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

さらに、Goodnotes ウェブアプリを開くと、Wasm バイナリ サイズが原因で、初期のダウンロード費用が 40 MB ほどかかります。当初、Goodnotes チームは、アプリバンドル自体と、使用するほとんどの API エンドポイントの通常のブラウザ キャッシュにのみ依存していましたが、後から考えると、より信頼性の高い Cache API とサービス ワーカーを早い段階で利用できたはずです。当初、チームは複雑さからこのタスクを避けていました。しかし、最終的には Workbox によってタスクが大幅に簡素化されたことに気づきました。

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

再利用したいコードが大量に含まれている iOS アプリがある場合は、準備を整えてください。素晴らしい旅が始まります。開始前に、役立つヒントをいくつかご紹介します。

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

まとめ

複雑なテクノロジー スタックを使用してウェブ プロジェクトを構築し、課題に満ちたプロダクトに取り組むのは、素晴らしい経験です。大変な作業ですが、必ず報われます。Goodnotes は、このアプローチを使用せずに、iOS アプリの新機能の開発を進めながら、Windows、Android、ChromeOS、ウェブ向けのバージョンをリリースすることはできませんでした。このテクノロジー スタックと Goodnotes のエンジニアリング チームのおかげで、Goodnotes はあらゆるデバイスで利用可能になりました。チームは次の課題に取り組む準備ができています。このプロジェクトについて詳しくは、Goodnotes チームが NSSpain 2023 で行った講演をご覧ください。ぜひ Goodnotes for web をお試しください。