QUEST LOG · June 2, 2026· UPDATED October 29, 2026
優雅に橋を渡る方法:SwiftUIを捨ててExpoを選ぶ
クロスプラットフォームは絶対にカクつくし、絶対に遅いものだとずっと思い込んでいました。でも最近、Expo とそのエコシステムのツールを本気で使ってみたら、実際はそうではありませんでした。
Solid(ar)ity(自分で鍵を握るタイプのプロフィールページ兼名刺アプリ)を作り始めたとき、最初のバージョンは SwiftUI で書いていました。2025 年 9 月に書き始めて、ネイティブで、きれいで、Apple 純正。当時はこれこそが正解だと思っていました。
ところが 2026 年 5 月末に Expo のプロジェクトを立ち上げて、6 月末には SwiftUI のアプリも、Xcode プロジェクトも、Swift の設定も全部捨ててしまいました。1 か月で、まるごと橋を渡りきりました(功績の大半はたぶん Claude ですが)。
この記事では、なぜ渡ったのか、どう渡ったのか、そして渡ったあとに気づいたことを記録しておきます。
# なぜ橋を渡るのか
一番直接的な理由は Android です。名刺や身分証明のようなものは、相手がどのスマホを使っているかなんてこちらではどうにもできません。iOS だけだと、半分の人はあなたのカードを読み取れないということです。SwiftUI でいくなら Android は丸ごと書き直しになり、2 つのコードベースで 2 セットの暗号ロジックを同期し続けることになる。考えただけで頭が痛くなります。
しかも、うちのコア機能である AirDrop に Pixel が突然対応しました。新しいもの好きの好奇心の塊としては、すぐに全機能をそこに載せて、本当に対応しているのか試してみました(あとで Pixel を持っている友達が対応してたと言ってくれました XD)。
2 つ目の理由はイテレーションの速さです。SwiftUI の Preview は動いたり動かなかったりで、UI を少し変えるだけでコンパイル待ちが長すぎて人生を疑うレベル。Expo なら保存した瞬間に画面が変わります。UI を変更するたびに完全に修行状態でしたし、しかも SwiftUI のほうが速いわけでもなく、あちこちで UIKit に戻る必要がありました……。
# 橋を渡る恐怖:クロスプラットフォームで起きると思っていた問題
React Native に対するイメージは、数年前で止まっていました。
- bridge が遅くて、アニメーションがカクつく
- ネイティブ機能(カメラ、Keychain、Face ID)に触れた途端、native module を自分で書く羽目になって泣く(今は AI がいるので助かりましたw)
- 暗号やゼロ知識証明みたいな重い計算は、JS では絶対に耐えられない。しかもこっちは ZK までやっている
ところが実際に使ってみたら、これらの問題には今どれも解決策がありました。
# 実際の解決策:Nitro を食らえ、小僧
1. ネイティブ部分は Nitro Modules で書く
パスポートの NFC 検証、Keychain、Apple Vision の OCR など、ネイティブに触れる必要があるところは、すべて Nitro Module として書いています。TypeScript で仕様を書くと、codegen が C++/Swift/Kotlin の骨組みを生成してくれるので、そこに実装を埋めるだけ。JS からネイティブを呼ぶのが普通の関数を呼ぶような感覚で、昔の bridge のようなシリアライズのオーバーヘッドもありません。
2. カメラは VisionCamera v5
QR スキャンもパスポートの MRZ 認識も、VisionCamera の frame processor 頼みです。ここで踏んだ地雷は記事がもう 1 本書けるほど多くて(実際に別で書きました。〈難易度・地獄:スマホカメラのパフォーマンス最適化〉)。
3. 重い計算はネイティブに置き、JS はフローだけを担当する
ZK 証明は Mopro でビルドした xcframework を使ってネイティブ側で実行し、JS は結果を受け取るだけ。パフォーマンスは純ネイティブと同じです。だってもともとネイティブなので。
# 橋から落ちずに渡るには
一番怖かったのは、旧バージョンのアプリのユーザーデータをどうするかです。暗号化のフォーマットが違えば、バックアップは全部パーになります。
私たちがやったのは、まず突き合わせテストを書くことでした。Swift 版に FixtureExporter を追加して、AES-GCM で暗号化した結果と名刺のデータフォーマットをテストデータとして書き出し、Expo 版では bun test で同じデータを回して、両方で暗号化したものが完全に一致し、お互いに復号できることを確認しました。突き合わせが全部通ってから、ようやく Swift 版を消す決心がつきました。
そしてこれが実際に役立ちました。Swift 時代に残っていたバックアップファイルは新バージョンとフォーマットが微妙に違っていて、新バージョンで古いバックアップを読めるように修正を一つ足すことになりました。突き合わせを先にやっていなかったら、この手の問題はリリース後にユーザーから怒られて初めて発覚していたと思います。
# 橋を渡ったあと
今の Solid(ar)ity 2.0 は Expo SDK 57、React Native 0.86 で、iOS と Android が 1 つのコードベースです。気づいたことがいくつかあります。
- クロスプラットフォーム=遅い、ではない。遅いのはたいてい、重いものを JS で走らせているから。ネイティブがやるべきことはネイティブに任せ、JS はフローと UI だけを担当すれば、体感はネイティブアプリとほぼ変わらない
- でもネイティブの知識が要らなくなるわけではない。Xcode Cloud のビルド、Metal シェーダーの事前コンパイル、Android の minSdk。落とし穴は一つも減っていない。「アプリ全体が落とし穴」から「境界に落とし穴がある」に変わっただけ
- SwiftUI が悪いわけではない。iOS だけで、しかも Apple の新しい API をがっつり使うなら、今でも最良の選択。私のアプリがたまたまそういうタイプではなかっただけ
というわけで、タイトルには「優雅に橋を渡る」と書きましたが、実際はまったく優雅ではありませんでした。まず両岸のものを揃えて、確実に渡れることを確かめてから、古い橋を壊しただけです。優雅に見えるのは、先にまったく優雅じゃない突き合わせをやったからです。
ちなみに、Solid(ar)ity の中にはまだ Swift のファイルがまったく優雅じゃない感じでたくさん詰め込まれています XD
公開中のアプリ
iOS・Android で無料。


