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層の問題が見つかった。

  1. アクセストークンの期限切れ — Google OAuthのトークンは約1時間で失効する。getAccessToken()がリフレッシュせずに失効済みトークンを返していた
  2. HTTPステータスコードの未チェック — Drive APIが401を返しても、レスポンスボディをそのままJSONデコードしようとして、紛らわしいエラーメッセージになっていた
  3. エラーの握り潰し — 一覧画面のプルリフレッシュは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固有の設定が足りない。

テンプレートの問題点:

代わりに、プロジェクトに合わせたシンプルなワークフローを書いた。

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つ。

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との開発ワークフロー

今回確立したワークフローはこうだ。

  1. 人間が問題を報告する(スクリーンショット、エラーメッセージ、操作手順)
  2. AIがコードを読んで原因を特定し、修正を実装する
  3. AIがGitHub Issueを作成し、ブランチを切り、PRを出す
  4. CIが自動でビルドチェックする
  5. AIがCI通過を確認してマージする
  6. 人間が実機で動作確認する

このループを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の互換性問題を検出したし、ローカライゼーションキーの衝突はシミュレータで実際に動かすまで気づけなかった。自動テストと実機確認は依然として人間の責任だ。

次のステップは、同期サービスのユニットテストを追加すること。アプリの最も複雑な部分にテストがない状態は、まだ解消されていない。