Cursor × iOSアプリ開発:クラッシュ修正からCI構築まで、AIと進めた1日の記録
はじめに
個人開発しているiOSメモアプリがある。SwiftUI + SwiftDataで作り、Google Driveにデータを同期する仕組みだ。TestFlightでベータテスト中に、同期周りでいくつか問題が見つかった。
今回は、CursorのAIエージェント(Claude)と一緒にこれらの問題を修正し、ついでにCI構築やUI改善まで進めた1日の記録をまとめる。
AIに「お願いします」と言うだけで勝手にコードが出てくる、という話ではない。人間がフィードバックを出し、AIがコードを書き、CIが検証する――このループをどう回したかの実践記録だ。
発端:「同期に失敗しました」
TestFlightユーザーから同期エラーの報告があった。設定画面で「今すぐ同期」を押すと、こんなメッセージが出る。
「データが見つからないため、読み込めませんでした。」
Googleアカウントにはサインイン済み。Driveにもデータはある。一見すると原因不明だ。
原因の特定
Cursorに同期サービスのコードを読ませ、エラーの発生パスを追跡してもらった。結果、3層の問題が見つかった。
- アクセストークンの期限切れ — Google OAuthのトークンは約1時間で失効する。
getAccessToken()がリフレッシュせずに失効済みトークンを返していた - HTTPステータスコードの未チェック — Drive APIが401を返しても、レスポンスボディをそのままJSONデコードしようとして、紛らわしいエラーメッセージになっていた
- エラーの握り潰し — 一覧画面のプルリフレッシュは
catch { print(...) }で処理しており、ユーザーには何も通知されていなかった
修正
修正自体はシンプルだった。
// Before: 失効済みトークンをそのまま返す
guard let token = currentUser?.accessToken.tokenString
else { throw GoogleAuthError.noAccessToken }
return token
// After: 毎回リフレッシュを試みる
try await user.refreshTokensIfNeeded()
guard let token = currentUser?.accessToken.tokenString
else { throw GoogleAuthError.noAccessToken }
return token
1行の追加で根本原因が解消する。だが、この1行に到達するまでに「エラーメッセージの実態はDecodingError」「401がfileNotFoundに化けている」という2段階の誤認を解く必要があった。
教訓: OAuth連携のあるアプリでは、トークンリフレッシュは初期実装の時点で入れるべき。1時間で切れるという仕様は基本中の基本だが、開発中はサインインし直すだけで済むため見落としやすい。
GitHub Actions CIの導入
修正のついでに、GitHub Actionsでビルドチェックを自動化した。GitHubが提案するテンプレート(Xcode - Build and Analyze)は汎用的すぎて、iOS固有の設定が足りない。
テンプレートの問題点:
-destinationが未指定(iOSアプリなのにシミュレータ指定がない)- スキーム検出にRubyを使う不安定なロジック
- コード署名の扱いがない
代わりに、プロジェクトに合わせたシンプルなワークフローを書いた。
name: Xcode Build
on:
push:
branches: [main]
pull_request:
branches: [main]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
name: Build
runs-on: macos-15
steps:
- uses: actions/checkout@v4
- name: Build
run: |
xcodebuild clean build \
-project 60dMemo.xcodeproj \
-scheme 60dMemo \
-destination 'platform=iOS Simulator,name=iPhone 16,OS=latest' \
-skipPackagePluginValidation \
CODE_SIGNING_ALLOWED=NO | xcpretty && exit ${PIPESTATUS[0]}
ポイントは3つ。
CODE_SIGNING_ALLOWED=NO— CI環境ではコード署名が不要concurrency— 同じPRの古いビルドを自動キャンセルmacos-15— Xcode 16 + iOS 18 SDKが使えるランナーを指定
CIが初日から価値を発揮した話
UI改善のPRで、フローティングボタンの背景色に.accentを使った。ローカル(Xcode 26.3)ではビルドが通ったが、CI(macos-15 / Xcode 16)ではビルドが失敗した。
❌ type 'ShapeStyle' has no member 'accent'
.accentはXcodeの新しいバージョンで追加されたAPIで、CIのXcodeバージョンでは使えなかった。Color.accentColorに差し替えて解決。
CIがなければ、このままmainに入っていた。CI導入の効果を初日に実感できたのは良い体験だった。
AIとの開発ワークフロー
今回確立したワークフローはこうだ。
- 人間が問題を報告する(スクリーンショット、エラーメッセージ、操作手順)
- AIがコードを読んで原因を特定し、修正を実装する
- AIがGitHub Issueを作成し、ブランチを切り、PRを出す
- CIが自動でビルドチェックする
- AIがCI通過を確認してマージする
- 人間が実機で動作確認する
このループを1日で6回以上回した。Issue → PR → Merge のトレーサビリティが確保されるため、後から「なぜこの変更をしたのか」が追える。
Cursor Rulesによるルール化
このワークフローをAIが毎回守るよう、.cursor/rules/にルールファイルを作成した。
# .cursor/rules/github-workflow.mdc
### main への直接コミット可否
| 変更内容 | 直接コミット | Issue/PR 必須 |
|-------------------------------|------------|-------------|
| README・ドキュメントのみの更新 | OK | — |
| .cursor/rules/ の更新 | OK | — |
| CI 設定の更新 | OK | — |
| Swift コードの変更(1行でも) | 禁止 | 必須 |
ルールファイルはリポジトリにコミットしてあるため、別のマシンでCursorを開いても同じルールが適用される。
.cursor/rules/はCursor固有の仕組みだが、ルールの内容自体はテキストなので、将来Claude Codeなど別のツールに移行する際にも流用できる。
見つかった問題と教訓
1日の作業を振り返って、いくつか教訓が得られた。
1. ローカライゼーションキーの衝突
ソート順の選択肢として"Title"、テキストフィールドのプレースホルダーとしても"Title"というキーを使っていた。日本語翻訳でソート順は「名前順」にしていたため、プレースホルダーにも「名前順」と表示されてしまった。
対策: 用途が異なる場合はキーを分ける。ソート順は"Sort by Title"、プレースホルダーは"Memo Title"とした。
2. print()で済ませるエラーハンドリング
開発中はcatch { print(error) }で十分に思える。だが、そのままリリースすると問題の発見が遅れる。プルリフレッシュの同期エラーが表示されない問題は、まさにこのパターンだった。
対策: リリース前にprintによるエラーハンドリングを全件レビューする。
3. CIとローカルのXcodeバージョン差異
ローカルでは最新のXcodeを使っていても、CI(やユーザーの端末)は異なるバージョンかもしれない。デプロイメントターゲットに合わせたAPIのみを使う意識が必要。
1日の成果
| カテゴリ | 内容 |
|---|---|
| バグ修正 | OAuthトークンリフレッシュ、HTTPステータスチェック、エラー表示改善 |
| CI構築 | GitHub Actionsによる自動ビルドチェック |
| UI改善 | フローティングボタン、エディタ統合、プレースホルダー修正 |
| ワークフロー | Issue/PR必須ルール、Cursor Rules整備 |
| コード品質 | ローカライゼーションキー修正、スキップ数追跡 |
Issue 4件、PR 4件、すべてCIを通してマージした。人間がやったのは「問題の報告」「方針の判断」「実機での確認」の3つだけ。コードを書く作業はほぼすべてAIが担当した。
まとめ
AIコーディングツールの真価は、コードを生成することそのものではなく、人間のフィードバックループを高速に回せることにあると感じた。
「ここがおかしい」と伝えれば原因を特定し、修正を実装し、Issue/PRを作り、CIを待ってマージする。人間は判断に集中できる。
一方で、AIが出したコードを鵜呑みにはできない。今回もCIが.accentの互換性問題を検出したし、ローカライゼーションキーの衝突はシミュレータで実際に動かすまで気づけなかった。自動テストと実機確認は依然として人間の責任だ。
次のステップは、同期サービスのユニットテストを追加すること。アプリの最も複雑な部分にテストがない状態は、まだ解消されていない。