前回の記事(リンク)では、AIでアプリをリリースした体験と気持ちについて書いた。今回はその技術的な補足として、実際にどういう構成で作ったのか、何が起きたのかを書く。
作ったもの
競馬の発走通知アプリ「まもなく発走」。指定したレースの発走時刻にAlarmKitでアラームを鳴らす。開発にはClaude Codeを使った。
技術スタック
v1.0.0時点の構成はこうなっている。
- iOSアプリ: SwiftUI + SwiftData + AlarmKit
- データ配信: AWS S3 + CloudFront(静的JSON)
- インフラ: Terraform(AWS)
- CI: Xcode Cloud
APIサーバーはない。レースデータのJSONをS3に置いて、CloudFront経由でiOSアプリが取得するだけのサーバーレス構成。通知のスケジューリングもすべてiOS側でローカルに行っている。
11日間で何が起きたか
209コミット、11日間の記録。
Day1: init → AlarmKit + Live Activityが動くまで 初コミットからAlarmKitでアラームが鳴り、Live Activityでカウントダウンが表示されるところまで。20コミット。初日にアプリの核となる機能が動いたのはAIエージェントの力を実感した瞬間だった。
Day2-3: アーキテクチャ整理 機能が動いた勢いのまま手動テストで進めていたが、このあたりで無理が出てくる。Claude Codeに任せるままに書いたコードがSwiftUIとSwiftDataの密結合を生んでおり、変更のたびに手で確認する範囲が広がっていた。Service/Repositoryレイヤーの分離を開始。61コミット。現在はTDDの採用やハーネスを用意するスタイルに落ち着いているが、当時はそこまでの知見がなかった。
Day4-5: インフラ構築 Terraformを書いてAWS環境を構築。S3 + CloudFrontの静的配信構成。CloudFrontからレースデータを取得できるようになり、テストデータからの脱却。26コミット。
Day6-8: UIブラッシュアップとLive Activity封印の伏線 SwiftDataのEntityとViewの分離(値型の導入)、UIの磨き込み。Live Activityのローカル更新が不安定だという問題が無視できなくなってくる。68コミット。
Day9-11: Live Activity封印、ストア申請、リリース Live Activityの機能をPhase 2まで無効化する判断を下す。プライバシーポリシーの整備、バンドルID修正、Xcode Cloudの設定など、ストア申請に必要な周辺作業。34コミット。
ハマったこと
Live Activityのローカル更新がずれる
一番テンションが上がった機能であり、一番悔しかった機能でもある。
Live Activityでレースまでの残り時間をカウントダウン表示する機能を作った。開発中に実機で動かしたときは「これは便利だ」と素直に感動した。
ところが、ローカルでのタイマー更新が不安定だった。バックグラウンドでの更新タイミングがOSに左右されるため、カウントダウンがずれる。レースの発走時刻を過ぎてもカウントダウンが止まらず、-00:20のような表示になる。通知アプリでこれは致命的だ。
結局、v1.0.0ではLive Activity機能を丸ごと封印してリリースした。この問題はサーバーからAPNsのリモートプッシュでLive Activityを更新する方式でないと根本的に解決できない。現在Phase 2としてサーバーサイドを開発しているのは、主にこの機能を実現するためだ。
Claude Codeに任せたらSwiftUIとSwiftDataが密結合した
これはAIエージェントとの開発ならではの教訓だと思う。
Claude Codeに最初の実装を任せたところ、SwiftDataの@QueryをView内で直接使う構成になった。Apple公式のサンプルコードがこのパターンを推奨しているし、AIとしては妥当な選択だったのだろう。
問題は、このパターンだとViewがSwiftDataのEntityに直接依存し、テストが書きづらくなることだ。@QueryはSwiftUIの環境に依存するため、ビジネスロジックがViewの中に閉じ込められてしまい、ロジック単体でのユニットテストが書けない。
11日間の中でService/Repositoryレイヤーの分離やEntityから値型への変換など色々こねくり回したが、結局しっくりくる構成にはならなかった。MVVM + Clean Architecture風の構成に書き換えたのはv1.0.0のリリース後のことだ。
SwiftUIの文脈では「ViewModelは不要」という意見をよく見かける。@Queryで直接データを取れるのだからViewModelは余計なレイヤーだ、という主張だ。実際にその構成で作ってみた感想としては、嘘だと思う。少なくとも、テストを書きたいなら。(それかViewModelが同じものを指していない)
AIエージェントとの開発で感じたこと
速い。圧倒的に速い。 初日にAlarmKitとLive Activityが動いたのは、自力では考えられないスピードだった。未経験の技術領域(AlarmKit、Terraform、CloudFront)に躊躇なく踏み込めるのは大きい。
ただし、長期的な設計は人間が導く必要がある。 @Query密結合の件でいえば、早く動くものを作るという点ではAIの判断は妥当だった。問題はその先で、プロダクトの長期的な展望に基づいてアーキテクチャを方向づけるのは人間の仕事だ。AIは指示すればそれに沿ったコードを書けるが、どこへ向かうべきかは自分で決めなければならない。動くものが出てくるスピードが速いぶん、立ち止まって方向を確認する意識が重要になる。
11日間の内訳を振り返ると、「作る」より「直す」の方が多かった気がする。 AIが高速に作ったものを、人間がレビューして、構造を直して、判断を下す。そのサイクルの繰り返しだった。


![[改訂新版]Swift実践入門 ── 直感的な文法と安全性を兼ね備えた言語 (WEB+DB PRESS plus) [改訂新版]Swift実践入門 ── 直感的な文法と安全性を兼ね備えた言語 (WEB+DB PRESS plus)](https://images-fe.ssl-images-amazon.com/images/I/51WsZJ6wtIL._SL160_.jpg)