取り組みの背景 — なぜ Claude Code × iOS なのか
iOSアプリ開発は年々複雑化しています。SwiftUI・Swift Concurrency・Xcode 16のマクロ、そしてApp Store審査基準の頻繁な更新。個人開発者が品質を保ちながらリリースサイクルを短縮するのは、もはや人力だけでは限界に近づいています。
Claude Codeは、そのボトルネックを打破する強力な武器です。単なる「コード補完」とは次元が違います。プロジェクト全体のコンテキストを把握し、アーキテクチャ設計からユニットテスト生成、App Store申請の説明文まで、開発の全工程に入り込んできます。
ここで扱うのはXcodeプロジェクトの初期設定からApp Store公開まで、Claude Codeを最大限活用した本番品質のiOSアプリ開発ワークフローを体系的に解説します。AndroidガイドとしてClaude Code × Android/Kotlin 本番アプリ開発完全ガイドもあわせてご参照いただけます。
この記事で習得できること:
- CLAUDE.mdを使ったiOSプロジェクト専用のAIコンテキスト設計
- SwiftUI × Claude Codeの効果的なプロンプト戦略
- Clean Architecture(MVVM + Repository)の実装パターン
- XCTest・XCUITestの自動生成
- Fastlane × GitHub Actions による完全CI/CD
- AdMob・In-App Purchase実装の効率化
対象読者: Swiftの基礎知識があり、Claude Codeの基本操作を経験済みの個人開発者〜スモールチーム
Step 1: 環境構築 — Xcode と Claude Code の連携
必要なツール
Claude Codeを使ったiOS開発環境を構築するために必要なツールを揃えます。
- Xcode 16以降(App Store または Apple Developer サイト)
- Claude Code CLI(
npm install -g @anthropic-ai/claude-code)
- Homebrew + Fastlane(
brew install fastlane)
- GitHub CLI(
brew install gh)
ターミナルとXcodeの連携設定
Claude Codeはターミナルから起動します。Xcodeプロジェクトのルートディレクトリで claude コマンドを実行することで、プロジェクト全体をコンテキストに取り込みます。
# プロジェクトルートに移動
cd ~/Projects/MyApp
# Claude Code を起動
claude
# プロジェクト構造を把握させる
> このプロジェクトのアーキテクチャを説明してください
MCP(Model Context Protocol)の設定
Claude Code用のMCPを使うと、Xcode Simulatorの操作やビルドログの解析まで自動化できます。~/.claude.json に以下を追加します。
{
"mcpServers": {
"xcode": {
"command": "npx",
"args": ["-y", "@anthropic/mcp-xcode"],
"env": {
"XCODE_PROJECT_PATH": "/Users/yourname/Projects/MyApp/MyApp.xcodeproj"
}
},
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/yourname/Projects"]
}
}
}
これにより、Claude Codeがビルドエラーを直接確認しながらコードを修正できるようになります。
Step 2: CLAUDE.md の設計 — iOS プロジェクト専用コンテキスト
CLAUDE.mdはClaude Codeが最初に読み込む「プロジェクト説明書」です。iOS開発においてこのファイルの設計品質が、AIアシスタンスの精度を大きく左右します。
CLAUDE.md テンプレート(iOS向け)
# MyApp — CLAUDE.md
## プロジェクト概要
- アプリ名: MyApp(壁紙・癒し系アプリ)
- 対象プラットフォーム: iOS 17以降 / iPadOS 17以降
- 最小サポートバージョン: iOS 16
- 開発言語: Swift 6.0 / SwiftUI 6
- アーキテクチャ: MVVM + Clean Architecture
- パッケージマネージャ: Swift Package Manager (SPM)
## ディレクトリ構成
MyApp/
├── App/ — AppDelegate, SceneDelegate, エントリポイント
├── Features/ — 機能別モジュール(Home, Wallpaper, Settings)
│ └── Wallpaper/
│ ├── View/ — SwiftUI Views
│ ├── ViewModel/ — ObservableObject
│ ├── Model/ — データモデル
│ └── Repository/ — データアクセス層
├── Core/ — 共通ユーティリティ、拡張
├── Services/ — AdMob, IAP, Analytics
└── Resources/ — Assets, Localizable.strings
## コーディング規約
- Swift 6のStrict Concurrencyを完全遵守(@MainActor, Sendable)
- プロパティラッパー: @State, @Binding, @Environment, @StateObject
- 非同期処理: async/await(コールバック禁止)
- エラーハンドリング: Result型または throws、クラッシュ禁止
## 禁止事項
- UIKit直接利用(SwiftUI専用)
- メインスレッドでの重い処理
- ハードコードされた文字列(Localizable.stringsを使用)
- 強参照サイクル([weak self]を適切に使用)
## 外部ライブラリ
- Google Mobile Ads SDK(AdMob)
- StoreKit 2(In-App Purchase)
- Kingfisher(画像キャッシュ)
- Swift Data(データ永続化)
## テスト戦略
- ユニットテスト: XCTest(ViewModel・Repository層)
- UIテスト: XCUITest(主要フロー)
- カバレッジ目標: 70%以上
## CI/CD
- GitHub Actions → TestFlight → App Store
- Fastlane match(証明書管理)
- 自動バージョンアップ: fastlane bump_build
このCLAUDE.mdを配置するだけで、Claude Codeはプロジェクトの文脈を完全に理解した上でコードを生成・修正します。
Step 3: SwiftUI × Claude Code の実践プロンプト戦略
効果的なプロンプトパターン
Claude Codeに対する指示の質が、コード品質を直接決定します。以下の3つのパターンを使い分けてください。
パターン1: 新機能の実装依頼
ユーザーが壁紙を「お気に入り」に追加できる機能を実装してください。
要件:
- SwiftDataを使って永続化
- ハートアイコンのトグルアニメーション
- 最大100件まで保存
- 削除機能付き
CLAUDE.mdのアーキテクチャに従って、View/ViewModel/Repositoryの3層で実装してください。
パターン2: バグ修正の依頼
以下のエラーが出ています:
[エラーメッセージをそのままペースト]
ファイル: Features/Wallpaper/View/WallpaperDetailView.swift
問題の箇所: 画像の非同期読み込み中にメモリが増加し続ける
原因を特定して修正してください。修正後にテストコードも追加してください。
パターン3: リファクタリングの依頼
HomeViewModel.swift のコードをレビューして、以下の観点で改善してください:
1. Swift 6のStrict Concurrency準拠
2. @MainActorの適切な配置
3. メモリリークの可能性
4. テスタビリティの向上
改善後のコードとその理由を説明してください。
SwiftUI View の生成例
実際にClaude Codeが生成するSwiftUIコードの例です:
// Claude Codeが生成したWallpaperCardView
import SwiftUI
import Kingfisher
struct WallpaperCardView: View {
let wallpaper: Wallpaper
@ObservedObject var viewModel: WallpaperListViewModel
@State private var isAnimating = false
var body: some View {
ZStack(alignment: .topTrailing) {
// 壁紙サムネイル
KFImage(wallpaper.thumbnailURL)
.resizable()
.aspectRatio(9/16, contentMode: .fill)
.clipped()
.cornerRadius(12)
.shadow(radius: 4)
// お気に入りボタン
Button {
withAnimation(.spring(response: 0.3, dampingFraction: 0.6)) {
isAnimating = true
}
Task {
await viewModel.toggleFavorite(wallpaper)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
isAnimating = false
}
} label: {
Image(systemName: wallpaper.isFavorite ? "heart.fill" : "heart")
.foregroundStyle(wallpaper.isFavorite ? .red : .white)
.font(.title2)
.scaleEffect(isAnimating ? 1.3 : 1.0)
.padding(8)
.background(.ultraThinMaterial, in: Circle())
}
.padding(8)
}
}
}
// プレビュー(Claude Codeが自動生成)
#Preview {
WallpaperCardView(
wallpaper: .preview,
viewModel: WallpaperListViewModel.preview
)
.frame(width: 180, height: 320)
}
期待する動作: タップするとハートアイコンがアニメーションしながら切り替わり、SwiftDataに即座に保存されます。
Step 4: Clean Architecture の実装パターン
MVVM + Repository パターン
Claude Codeは指定したアーキテクチャに忠実にコードを生成します。ViewModel層とRepository層を分離することで、テスタビリティが大幅に向上します。
// Repository プロトコル(Claude Codeが設計)
protocol WallpaperRepositoryProtocol: Sendable {
func fetchWallpapers(category: WallpaperCategory) async throws -> [Wallpaper]
func toggleFavorite(_ wallpaper: Wallpaper) async throws
func fetchFavorites() async throws -> [Wallpaper]
}
// 本番実装
@MainActor
final class WallpaperRepository: WallpaperRepositoryProtocol {
private let modelContext: ModelContext
private let apiClient: WallpaperAPIClient
init(modelContext: ModelContext, apiClient: WallpaperAPIClient = .shared) {
self.modelContext = modelContext
self.apiClient = apiClient
}
func fetchWallpapers(category: WallpaperCategory) async throws -> [Wallpaper] {
// キャッシュ確認 → APIフォールバック
let cached = try fetchCached(category: category)
if !cached.isEmpty { return cached }
let remote = try await apiClient.fetchWallpapers(category: category)
try cacheWallpapers(remote)
return remote
}
func toggleFavorite(_ wallpaper: Wallpaper) async throws {
wallpaper.isFavorite.toggle()
try modelContext.save()
}
func fetchFavorites() async throws -> [Wallpaper] {
let descriptor = FetchDescriptor<Wallpaper>(
predicate: #Predicate { $0.isFavorite },
sortBy: [SortDescriptor(\.favoritedAt, order: .reverse)]
)
return try modelContext.fetch(descriptor)
}
private func fetchCached(category: WallpaperCategory) throws -> [Wallpaper] {
let descriptor = FetchDescriptor<Wallpaper>(
predicate: #Predicate { $0.categoryRaw == category.rawValue }
)
return try modelContext.fetch(descriptor)
}
private func cacheWallpapers(_ wallpapers: [Wallpaper]) throws {
wallpapers.forEach { modelContext.insert($0) }
try modelContext.save()
}
}
// テスト用モック(Claude Codeが自動生成)
final class MockWallpaperRepository: WallpaperRepositoryProtocol {
var wallpapersToReturn: [Wallpaper] = []
var shouldThrowError = false
private(set) var toggledWallpaper: Wallpaper?
func fetchWallpapers(category: WallpaperCategory) async throws -> [Wallpaper] {
if shouldThrowError { throw URLError(.notConnectedToInternet) }
return wallpapersToReturn
}
func toggleFavorite(_ wallpaper: Wallpaper) async throws {
if shouldThrowError { throw URLError(.badServerResponse) }
toggledWallpaper = wallpaper
}
func fetchFavorites() async throws -> [Wallpaper] {
return wallpapersToReturn.filter { $0.isFavorite }
}
}
ViewModel の設計
@MainActor
final class WallpaperListViewModel: ObservableObject {
@Published private(set) var wallpapers: [Wallpaper] = []
@Published private(set) var isLoading = false
@Published private(set) var error: Error?
private let repository: WallpaperRepositoryProtocol
init(repository: WallpaperRepositoryProtocol) {
self.repository = repository
}
func loadWallpapers(category: WallpaperCategory) async {
isLoading = true
error = nil
do {
wallpapers = try await repository.fetchWallpapers(category: category)
} catch {
self.error = error
}
isLoading = false
}
func toggleFavorite(_ wallpaper: Wallpaper) async {
do {
try await repository.toggleFavorite(wallpaper)
// ローカル状態を即時更新(楽観的更新)
if let index = wallpapers.firstIndex(where: { $0.id == wallpaper.id }) {
wallpapers[index].isFavorite.toggle()
}
} catch {
self.error = error
}
}
}
Step 5: テスト自動生成 — XCTest × Claude Code
効率的なテスト生成プロンプト
Claude Codeに以下のように依頼するだけで、網羅的なテストスイートが生成されます。
WallpaperListViewModel の単体テストを生成してください。
テスト対象のメソッド:
- loadWallpapers(成功・失敗・空データ)
- toggleFavorite(成功・失敗)
MockWallpaperRepository を使い、async/await パターンで記述してください。
@MainActor を適切に配置し、Xcode 16 で警告が出ないようにしてください。
Claude Codeが生成するテストコード:
import XCTest
@testable import MyApp
@MainActor
final class WallpaperListViewModelTests: XCTestCase {
var sut: WallpaperListViewModel!
var mockRepository: MockWallpaperRepository!
override func setUp() async throws {
mockRepository = MockWallpaperRepository()
sut = WallpaperListViewModel(repository: mockRepository)
}
override func tearDown() async throws {
sut = nil
mockRepository = nil
}
// MARK: - loadWallpapers テスト
func test_loadWallpapers_success_updatesWallpapers() async throws {
// Given
let expected = [Wallpaper.stub(id: "1"), Wallpaper.stub(id: "2")]
mockRepository.wallpapersToReturn = expected
// When
await sut.loadWallpapers(category: .nature)
// Then
XCTAssertEqual(sut.wallpapers.count, 2)
XCTAssertEqual(sut.wallpapers.map(\.id), ["1", "2"])
XCTAssertFalse(sut.isLoading)
XCTAssertNil(sut.error)
}
func test_loadWallpapers_failure_setsError() async throws {
// Given
mockRepository.shouldThrowError = true
// When
await sut.loadWallpapers(category: .nature)
// Then
XCTAssertTrue(sut.wallpapers.isEmpty)
XCTAssertNotNil(sut.error)
XCTAssertFalse(sut.isLoading)
}
func test_loadWallpapers_empty_showsNoWallpapers() async throws {
// Given: 空レスポンス
mockRepository.wallpapersToReturn = []
// When
await sut.loadWallpapers(category: .nature)
// Then
XCTAssertTrue(sut.wallpapers.isEmpty)
XCTAssertNil(sut.error)
}
// MARK: - toggleFavorite テスト
func test_toggleFavorite_success_updatesLocalState() async throws {
// Given
let wallpaper = Wallpaper.stub(id: "1", isFavorite: false)
sut.wallpapers = [wallpaper]
// When
await sut.toggleFavorite(wallpaper)
// Then
XCTAssertNotNil(mockRepository.toggledWallpaper)
XCTAssertTrue(sut.wallpapers.first?.isFavorite ?? false)
}
func test_toggleFavorite_failure_setsError() async throws {
// Given
let wallpaper = Wallpaper.stub(id: "1")
sut.wallpapers = [wallpaper]
mockRepository.shouldThrowError = true
// When
await sut.toggleFavorite(wallpaper)
// Then
XCTAssertNotNil(sut.error)
}
}
// テスト用スタブ
extension Wallpaper {
static func stub(id: String = UUID().uuidString, isFavorite: Bool = false) -> Wallpaper {
Wallpaper(id: id, title: "Test Wallpaper", isFavorite: isFavorite)
}
}
このように、Claude Codeはモックの生成からアサーションの設計まで、テストコードをほぼ完全に自動生成します。テストカバレッジを70%以上に保つことが、リリース後のバグ混入を防ぐ最短経路です。
Step 6: AdMob × In-App Purchase の実装
AdMob バナー広告の SwiftUI 統合
// Claude Codeが生成するAdMob統合コード
import GoogleMobileAds
import SwiftUI
struct AdBannerView: UIViewRepresentable {
let adUnitID: String
func makeUIView(context: Context) -> GADBannerView {
let bannerView = GADBannerView(adSize: GADAdSizeBanner)
bannerView.adUnitID = adUnitID
bannerView.rootViewController = UIApplication.shared.connectedScenes
.compactMap { $0 as? UIWindowScene }
.first?.windows.first?.rootViewController
bannerView.load(GADRequest())
return bannerView
}
func updateUIView(_ uiView: GADBannerView, context: Context) {}
}
// ContentView での使用例
struct ContentView: View {
var body: some View {
VStack {
WallpaperGridView()
Spacer()
// テスト用Unit ID(本番では Info.plist などで管理)
AdBannerView(adUnitID: ProcessInfo.processInfo.environment["ADMOB_BANNER_ID"] ?? "ca-app-pub-3940256099942544/2934735716")
.frame(height: 50)
}
}
}
StoreKit 2 による In-App Purchase
// StoreKit 2 実装(Claude Codeが設計)
import StoreKit
@MainActor
final class PurchaseManager: ObservableObject {
@Published private(set) var isPremium = false
@Published private(set) var isLoading = false
@Published private(set) var error: Error?
private let productIDs = ["com.yourapp.premium_monthly", "com.yourapp.premium_yearly"]
private var updateTask: Task<Void, Error>?
init() {
updateTask = Task { [weak self] in
await self?.listenForTransactions()
}
Task { await verifyPurchaseStatus() }
}
deinit { updateTask?.cancel() }
func purchase(_ product: Product) async {
isLoading = true
error = nil
do {
let result = try await product.purchase()
switch result {
case .success(let verification):
let transaction = try checkVerified(verification)
await transaction.finish()
await verifyPurchaseStatus()
case .userCancelled, .pending:
break
@unknown default:
break
}
} catch {
self.error = error
}
isLoading = false
}
private func verifyPurchaseStatus() async {
for await result in Transaction.currentEntitlements {
if case .verified(let transaction) = result {
isPremium = productIDs.contains(transaction.productID)
}
}
}
private func listenForTransactions() async {
for await result in Transaction.updates {
if case .verified(let transaction) = result {
await transaction.finish()
await verifyPurchaseStatus()
}
}
}
private func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
switch result {
case .unverified: throw StoreError.failedVerification
case .verified(let safe): return safe
}
}
}
enum StoreError: LocalizedError {
case failedVerification
var errorDescription: String? { "購入の検証に失敗しました" }
}
Step 7: Fastlane × GitHub Actions CI/CD
Fastfile の設定
# Fastfile(Claude Codeが生成・最適化)
default_platform(:ios)
platform :ios do
before_all do
setup_ci if ENV['CI']
end
desc "テスト実行"
lane :test do
run_tests(
scheme: "MyApp",
devices: ["iPhone 16"],
code_coverage: true,
output_directory: "fastlane/test_output"
)
end
desc "TestFlight にアップロード"
lane :beta do
increment_build_number(
build_number: latest_testflight_build_number + 1
)
match(type: "appstore", readonly: is_ci)
build_app(
scheme: "MyApp",
configuration: "Release",
export_method: "app-store",
export_options: {
provisioningProfiles: {
"com.yourcompany.myapp" => "match AppStore com.yourcompany.myapp"
}
}
)
upload_to_testflight(skip_waiting_for_build_processing: true)
end
desc "App Store に提出"
lane :release do
test
beta
deliver(
submit_for_review: true,
automatic_release: false,
submission_information: { add_id_info_uses_idfa: false }
)
end
end
GitHub Actions ワークフロー
# .github/workflows/ios.yml
name: iOS CI/CD
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: macos-15
steps:
- uses: actions/checkout@v4
- name: Xcode 選択
run: sudo xcode-select -s /Applications/Xcode_16.2.app
- name: Ruby セットアップ
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.3'
bundler-cache: true
- name: テスト実行
run: bundle exec fastlane test
- name: カバレッジレポート
uses: actions/upload-artifact@v4
if: always()
with:
name: test-results
path: fastlane/test_output/
deploy:
runs-on: macos-15
needs: test
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Ruby セットアップ
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.3'
bundler-cache: true
- name: TestFlight デプロイ
run: bundle exec fastlane beta
env:
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
MATCH_GIT_BASIC_AUTHORIZATION: ${{ secrets.MATCH_GIT_BASIC_AUTHORIZATION }}
APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
APP_STORE_CONNECT_API_KEY_CONTENT: ${{ secrets.APP_STORE_CONNECT_API_KEY_CONTENT }}
Step 8: よくあるエラーと Claude Code による自動修正
エラー1: Swift 6 Strict Concurrency 警告
症状: Sending 'self' risks causing data races
以下の Strict Concurrency エラーを修正してください:
[エラーをペースト]
@MainActor の適切な配置と Sendable 準拠で解決してください。
コンパイルエラーが残らないように確認してください。
Claude Codeが採用する修正パターン:
- クロージャ内で
[weak self] を使いつつ Task { @MainActor in ... } で囲む
Sendable プロトコルへの準拠追加(@unchecked Sendable は最終手段)
@MainActor のスコープを型全体ではなくメソッド単位に絞る
エラー2: SwiftData マイグレーション失敗
症状: アップデート後にアプリがクラッシュ(NSInternalInconsistencyException)
SwiftData のスキーマバージョン管理を実装してください。
既存モデル: Wallpaper(id, title, thumbnailURL)
新しい要件: favoriteCount フィールドを追加(デフォルト値 0)
VersionedSchema と MigrationPlan を使って安全に移行してください。
エラー3: App Store 審査リジェクト対策
Claude Codeは審査リジェクトのリスク評価も得意です。
以下のアプリ説明文を App Store 審査ガイドラインに準拠した形に修正してください:
[現在の説明文]
特に気をつけてほしい点:
- 機能の誇大表現を避ける
- 「ベスト」「最高」等の根拠なき表現の削除
- プライバシーポリシーへの適切な言及
- ATTフレームワーク使用の明示(AdMob使用アプリ向け)
エラー4: メモリ使用量の肥大化
壁紙アプリに多い問題のひとつが、大量の画像によるメモリ圧迫です。
WallpaperGridView でスクロール時にメモリが増え続けています。
LazyVGrid の中で KFImage を使っています。
メモリリークの原因を特定して修正してください。
Instruments のリークシミュレーション方法も合わせて教えてください。
まとめ
Claude Code × Swift/iOSの組み合わせは、個人開発者にとって最強の開発加速エンジンです。本記事で解説したポイントを振り返ります:
- CLAUDE.md設計 がAIアシスタンスの精度を決定する — 丁寧に書くほど返ってくるコードの品質が向上する
- 明確な3層アーキテクチャ(View/ViewModel/Repository)をCLAUDE.mdに明示することで、Claude Codeが一貫した設計でコードを生成する
- テスト自動生成はClaude Codeが最も得意とする領域 — ViewModelとRepository層のテストカバレッジを一気に高められる
- Fastlane × GitHub ActionsでCI/CDを自動化すると、mainブランチへのマージだけでTestFlightにアップロードされる理想的な環境を構築できる
- AdMob・StoreKit 2の実装パターンも、Claude Codeに任せることで正確かつ安全なコードを素早く得られる
次のステップとして、Claude API を iOS/SwiftUI アプリに組み込む完全ガイドでAnthropicのAPIをアプリに直接統合する方法を学ぶと、AI機能を搭載した次世代iOSアプリの開発が現実のものとなります。
Claude Codeと組み合わせることで、理解と実装の両面で学習速度が飛躍的に向上します。