既存アプリを「設計図」にして、AIで別スタックへ移植する ── SwiftUIアプリをFlutterに作り直した話

SwiftUI(wari-ios) から Flutter(soroe) への構造対応表による移植の図

はじめに

日程調整アプリ soroe(iOS / Android / Web の Flutter アプリ)は、真っ白な状態から設計したわけではない。既存の SwiftUI アプリ wari-ios(SwiftUI / MVVM)を**「設計図」として渡し、Claude で Flutter / Riverpod へ移植**して作った。

このシリーズでは、Claude Code と個人開発する仕組みを書いてきた。第1回はドキュメント(spec と ADR)、第2回は CLAUDE.md。今回はその上で、すでにある完成品を下敷きに、AIで別スタックへ作り直すという進め方を紹介する。

言語もフレームワークも違う(Swift → Dart、SwiftUI/MVVM → Flutter/Riverpod)。それでも移植がうまくいったのは、コードを1行ずつ翻訳させたからではなく、レイヤーの構造対応表を先に用意したからだった。


なぜゼロから作らず「移植」にするのか

wari-ios には、すでに動いている資産がある。

  • APIクライアントとエラー処理
  • トークンの保存とリフレッシュ
  • 画面とロジックの分離(MVVM)
  • 共通バックエンド ynetlabo-api を叩く一連の流れ

これを新アプリのためにゼロから設計し直すのは、車輪の再発明だ。しかも soroe は同じバックエンドを使う。だったら、やるべきは「新規設計」ではなく「翻訳」になる。

AIはこの「翻訳」が得意だ。ボイラープレートを一から考えさせると迷いや揺れが出るが、手本があれば、それを別の言語・フレームワークの語彙に置き換えるのは精度が高い。移植は、AI支援開発と特に相性がいい。


鍵は「構造対応表」を先に作ること

言語が違っても、アプリのレイヤーが担う役割は対応づく。SwiftUIのViewModelに相当するものは、Flutterでは何か。Codableに相当するものは何か。これを毎回Claudeに推測させると、同じ役割のコードが画面ごとに微妙に違う書き方になってしまう。

そこで、移植を始める前に CLAUDE.md構造対応表を書いた。soroeの場合はこうだ。

wari-ios(SwiftUI / MVVM)soroe(Flutter / Riverpod)
Services/APIClientlib/core/network/dio_client.dart
ViewModels/*ViewModel(ObservableObject)Riverpod の Notifier(*controller.dart
Models/*(Codable)手書きモデル(fromJson / toJson
Views/pagesViews/componentspresentation/core/widgets/
UserDefaults の authTokenflutter_secure_storageTokenStorage

この表があると、Claudeは「このSwiftのViewModelはFlutterで何になるか」を毎回考えない。決まった対応で機械的に置けるので、判断のブレが消え、アプリ全体の書き方が揃う。

対応表は、AIに渡す翻訳辞書だと考えると分かりやすい。辞書を最初に固めておくほど、以降の翻訳が安定する。


進め方:機能単位で「縦」に移植する

移植はファイル単位ではなく、機能単位で縦に進めた。soroeなら auth(認証)→ room(グループ)→ shift(シフト)という順で、1機能ずつ移していく。

各機能でやることは同じだ。

  1. wari-ios の該当機能のコードを読ませる(元の設計)
  2. 構造対応表に沿って Flutter で実装する(data / domain / presentation の縦割り)
  3. flutter analyzeflutter test を通す

第1回で書いた spec-driven とも噛み合う。移植先の「今の仕様」は docs/specs/ に書き、対応表で「どう置き換えるか」を固定する。元コードは設計図、specは現在地、対応表は翻訳辞書 ── 3つが揃うと、Claudeは迷いなく1機能を書き切れる。

横方向(全画面のAPIクライアントだけ先に、次に全画面のモデルだけ…)ではなく縦方向にしたのは、1機能ずつ動く状態で積み上げたいからだ。認証が通ってから次に進めば、途中で全体が壊れることがない。


「機械翻訳しない」境界を見極める

とはいえ、全部を機械的に写経したわけではない。そのまま移植してはいけない部分がある。ここはAIに丸投げせず、こちらで設計を判断した。

プラットフォーム差

wari-ios は iOS 専用だが、soroe は Web でも動く。LINEログインは、モバイルはネイティブSDK、Webは LIFF(LINEミニアプリ)と経路が分かれる。ここは「Swiftのコードを翻訳」では出てこない。条件付き import でプラットフォームごとに実装を隔離する、という移植先固有の設計が要る(→ ADR 0002)。

フレームワークのライフサイクル差

SwiftUIのObservableObjectとRiverpodのNotifierは「役割」は対応するが、破棄のタイミングが違う。RiverpodのautoDisposekeep-aliveは、SwiftUIにはない概念だ。画面から戻ったときにデータを取り直す挙動なども、Flutter側の事情に合わせて設計し直す必要があった(→ ADR 0003)。

対応表は9割の「翻訳」を自動化してくれる。だが残り1割の、移植先のフレームワークでしか発生しない判断は人間が握る。そしてその判断は、第1回で書いたとおり ADR に残す。ここの切り分けが、移植を「劣化コピー」にしないコツだ。


Claudeユーザへの持ち帰り

すでに動いているアプリが手元にあるなら、別スタックへの展開は「新規開発」より「移植」として捉えたほうが、AIの力を引き出せる。要点は3つ。

  1. 構造対応表を先に作る。 レイヤーの役割対応を CLAUDE.md に固定し、AIに毎回推測させない。
  2. 機能単位で縦に積む。 1機能ずつ動く状態にしながら移す。spec-driven と組み合わせる。
  3. 機械翻訳しない境界を決める。 プラットフォーム差・フレームワーク差は人間が設計し、理由を ADR に残す。

移植の9割は退屈な置き換えだ。そここそAIに任せ、人間は残り1割の判断に集中する。この分担が、既存資産を持っている個人開発者にとっていちばん効く。


まとめ

soroe は、wari-ios という「動く設計図」があったからこそ、短期間で iOS / Android / Web に載せられた。ゼロから設計する迷いがなく、バックエンドはそのまま流用でき、挙動の互換性も保ちやすい。

その移植を支えたのは、派手な仕組みではなく、CLAUDE.md に書いた一枚の構造対応表だった。AIに「翻訳」させるなら、まず辞書を渡す。既存資産を別スタックへ広げたい人に、参考になれば。


このシリーズ:第1回 spec と ADR第2回 CLAUDE.md の書き方/第3回(本記事)

← 記事一覧へ戻る