13 min read
Making Claude Code Work for iOS Development
Making Claude Code Work for iOS Development

If you’ve tried using Claude Code on an iOS project and thought “this doesn’t work as well as it does for web stuff,” you’re not imagining it. iOS development has quirks that trip up AI-assisted workflows in ways that JavaScript developers never encounter. Xcode is its own universe with its own gravity.

I’ve been using Claude Code for iOS work for a while now, and I’ve watched it corrupt project files, hallucinate SwiftUI APIs that don’t exist, and confidently write code for iOS 18 when my deployment target was iOS 15. But I’ve also seen it do things that would have taken me hours—refactoring entire view hierarchies, debugging async camera issues, generating comprehensive test suites. The difference between disaster and productivity comes down to setup and discipline.

Here’s what actually works.

The .pbxproj Problem

Your .xcodeproj contains project.pbxproj—a fragile file that tracks every source file, framework, and build setting. When Claude Code tries to edit it, things break. Mismatched UUIDs, corrupted XML, and suddenly Xcode won’t open your project.

The fix: never let Claude Code modify .pbxproj. Add this to your CLAUDE.md:

## Project File Rules
- NEVER modify any .pbxproj file directly
- Create new Swift/resource files in the correct directory
- I will add files to the Xcode project manually

Or better: use folder references instead of groups. Folder references (blue folders in Xcode’s navigator) track directories rather than individual files. Claude Code can freely create files, and the .pbxproj never changes.

As of Xcode 16, new projects default to folders. For older projects, right-click a group and select “Convert to Folder.”

The tradeoff is you can’t reorder files in the navigator or have a group structure that differs from your directory structure. For most projects, that’s fine.

Key insight: Folder references let the filesystem be the source of truth. Claude Code can freely create files without touching project metadata.

Closing the Feedback Loop

Here’s the thing about Claude Code that makes it genuinely useful: it can see the results of its work. When it writes Python, it can run the script and see the output. When it writes React, it can see the build errors. This feedback loop is what separates “AI that writes plausible-looking code” from “AI that writes code that actually works.”

For iOS, the feedback loop is broken by default. Claude Code can’t see Xcode errors. It can’t see your simulator. It can’t know that the build failed because it used a deprecated API.

The Basic Fix: Raw xcodebuild

Claude Code can run xcodebuild directly in the terminal. This works:

xcodebuild -scheme MyApp -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 15 Pro' build

Claude Code sees the compiler output, catches syntax errors, and can iterate. For simple projects, this might be enough.

The Better Fix: XcodeBuildMCP

XcodeBuildMCP wraps xcodebuild and adds everything else you need to actually run and debug your app, not just compile it:

  • Simulator control: Boot simulators, install the app, launch it
  • Screenshots: Capture what’s on screen so Claude Code can visually verify the UI
  • Runtime logs: Capture console output from the running app
  • UI automation: Interact with the simulator (taps, swipes) and inspect the accessibility hierarchy
  • Device deployment: Build and install to physical devices

Install it:

npx -y @smithery/cli@latest install cameroncooke/xcodebuildmcp --client claude-code

Or add it manually to your Claude Code MCP configuration:

{
  "mcpServers": {
    "XcodeBuildMCP": {
      "command": "npx",
      "args": ["-y", "xcodebuildmcp@latest"]
    }
  }
}

Once it’s running, Claude Code can do things like:

# Build and see errors
mcp__xcodebuildmcp__build_sim_name_proj

# Run tests
mcp__xcodebuildmcp__test_sim_name_proj

# Capture runtime logs
mcp__xcodebuildmcp__capture_logs

# Take simulator screenshots
mcp__xcodebuildmcp__screenshot

This transforms the workflow. Instead of Claude Code writing code, building, and stopping at “it compiles” — it can install the app, run it, capture logs when something crashes, and take screenshots to verify the UI looks right. The difference between “it builds” and “it works” is significant.

Key insight: Raw xcodebuild closes the compilation loop. XcodeBuildMCP closes the runtime loop.

The CLAUDE.md File: Your iOS-Specific Playbook

Every Claude Code session starts by reading CLAUDE.md in your project root. For iOS projects, this file needs to do more work than it does for typical web projects.

Here’s a starter template:

# Project: [Your App Name]

## Overview
- Platform: iOS [minimum version]
- Language: Swift [version]
- UI Framework: SwiftUI / UIKit
- Architecture: MVVM / TCA / etc.

## Build Commands
Use XcodeBuildMCP for all build operations:
- Build: `mcp__xcodebuildmcp__build_sim_name_proj`
- Test: `mcp__xcodebuildmcp__test_sim_name_proj`
- Clean: `mcp__xcodebuildmcp__clean_proj`

## Preferred Simulator
iPhone 15 Pro (UDID: [paste your simulator UDID here])

To find UDIDs: `xcrun simctl list devices available`

## Critical Rules
1. NEVER modify .pbxproj files
2. Create files in the correct directory structure; I will add them to Xcode
3. Always use async/await, not completion handlers
4. Use Swift's new Observation framework, not Combine, for new code
5. Target iOS [version]+ APIs only

## Project Structure
- Sources/
  - Features/        # Feature modules
  - Core/            # Shared utilities
  - Services/        # Network, persistence
- Tests/
- Resources/         # Assets, localizations

## Known Gotchas
- [Add platform-specific issues as you hit them]
- Example: "NO .background() before .glassEffect() on iOS 26"

The simulator UDID is worth calling out. Every time you start a session, Claude Code might need to figure out which simulator to use. Finding the UDID requires running a command that produces a wall of output. If you cache your preferred simulator’s UDID in CLAUDE.md, you skip this overhead on every session.

Nested CLAUDE.md Files

Here’s something that helps on larger projects: you can put additional CLAUDE.md files in subdirectories. Claude Code reads them when working in that part of the codebase.

For example, in a Views/ folder:

# Views

## Conventions
- All views use @Observable view models, not @ObservableObject
- Extract subviews when a body exceeds 50 lines
- Use ViewThatFits for adaptive layouts

## Common Patterns
- Loading states: Use ContentUnavailableView for empty states
- Error handling: Wrap in ErrorBoundaryView
- Navigation: Use NavigationStack with typed paths

## Gotchas
- .glassEffect() must be the last modifier
- Always test with Dynamic Type at accessibility sizes

Or in a Services/ folder:

# Services

## Patterns
- All network calls return async throws
- Use URLSession.shared, not custom sessions
- Errors should conform to LocalizedError

## Testing
- Mock services via protocol, inject in init
- Use MockURLProtocol for network tests

This keeps context-specific guidance close to the code it applies to. Claude Code picks it up automatically when working in those directories.

Building Institutional Memory

The real power of these files is the “Known Gotchas” sections. Every time Claude Code makes a mistake that’s platform-specific—uses a deprecated API, gets the modifier order wrong in SwiftUI, forgets that HealthKit requires specific entitlements—add it to the relevant CLAUDE.md. Claude Code will read it on the next session and won’t make that mistake again.

Key insight: CLAUDE.md is institutional memory. Every gotcha you document is a mistake you’ll never have to fix twice.

Context Management: The /compact and /clear Dance

Claude Code has a context window. When it fills up, Claude Code automatically runs /compact to summarize the conversation and free up space. This is fine for short sessions but problematic for iOS development where you’re often working across many files.

The issue: auto-compaction happens whenever the context fills, potentially in the middle of a complex operation. Claude loses track of what it was doing. It might forget that it already tried one approach and failed. It might re-introduce a bug it already fixed.

The fix: manage context proactively.

Early in development, when you’re building foundational architecture and Claude needs to understand many files, use /compact strategically. When you hit about 60% context usage, compact with specific instructions:

/compact Focus on the NetworkService implementation and
the authentication flow. Forget the UI work.

Later in development, when you’re making smaller, isolated changes, prefer /clear over /compact. Starting fresh with known context (your CLAUDE.md) is often faster than trying to preserve a degraded summary of a long conversation.

The pattern I’ve settled on: one logical task per session. Need to implement a new feature? Start a session, do the feature, commit, /clear. Need to fix a bug? Start a session, fix the bug, commit, /clear. This keeps context tight and prevents the weird errors that come from Claude Code half-remembering something from three hours ago.

Key insight: Small, focused sessions with aggressive clearing beats one long session with degraded context.

Keep Tasks Surgical

This is maybe the most important workflow habit: don’t ask Claude Code to “refactor the whole app.” Don’t ask it to “implement the settings screen and also fix that networking bug.”

Smaller scope equals better results. Always.

Good prompts for iOS work:

"Add a loading state to ProfileView. Show a ProgressView
while userData is nil."

"The camera capture is failing silently. Add Logger statements
to CaptureService.startSession() so we can see what's happening."

"Convert UserSettingsManager from @ObservableObject to use
the new @Observable macro."

Bad prompts:

"Build out the entire authentication flow with sign in,
sign up, password reset, and biometric unlock."

"Refactor the app to use SwiftData instead of Core Data."

"Fix all the compiler warnings."

The difference isn’t just about reliability. When tasks are small, you can review what Claude Code did. You can catch when it takes a weird approach before that approach propagates through your codebase. When tasks are huge, you’re essentially hoping for the best.

Key insight: If you can’t review the diff in under two minutes, the task was too big.

Request Debug Logging Explicitly

When Claude Code writes complex async code—camera capture, HealthKit queries, background tasks—explicitly ask it to add logging:

"Implement the HealthKit data sync, and add Logger statements
at every decision point so I can trace the execution path."

You will need this logging. Async iOS code fails in mysterious ways. Race conditions, missing entitlements, background execution limits—the failure modes are endless. Without logging, you’re debugging blind.

Claude Code is actually good at this when prompted. It’ll add structured logging with subsystem and category, use appropriate log levels, and include relevant context in the messages. But it won’t do it automatically.

Key insight: The five minutes Claude Code spends adding logging will save you hours debugging async issues.

SwiftUI Modifier Order

Claude Code knows SwiftUI, but it doesn’t always respect modifier order—and in SwiftUI, modifier order matters.

This works:

Text("Hello")
    .padding()
    .background(Color.blue)
    .cornerRadius(8)

This produces a different result:

Text("Hello")
    .background(Color.blue)
    .padding()
    .cornerRadius(8)

And in iOS 26 with the new Liquid Glass APIs, order is even more critical. Apple’s documentation explicitly states that .glassEffect() should be applied last, after other modifiers that affect appearance. Get it wrong and interactive states, sticky behaviors, and morphing animations break:

// ✅ Correct: glassEffect comes last
Text("Glass Button")
    .padding()
    .background(.ultraThinMaterial)
    .glassEffect(.clear, in: .capsule)

// ❌ Wrong: glassEffect before other appearance modifiers
Text("Broken Button")
    .glassEffect(.regular)
    .padding()
    .background(Color.blue)

If you’re seeing weird layout issues, broken animations, or visual bugs with Liquid Glass, modifier order is often the culprit. Add specific modifier ordering rules to your CLAUDE.md as you discover them.

Key insight: SwiftUI modifier order bugs look like Claude Code doesn’t understand SwiftUI. Usually it just put things in the wrong order.

A Practical Setup Checklist

Here’s what a productive Claude Code iOS setup looks like:

ComponentPurpose
XcodeBuildMCPBuild feedback loop, simulator control, screenshots
CLAUDE.md at project rootProject context, rules, gotchas
Nested CLAUDE.md filesDirectory-specific conventions
Simulator UDID cachedSkip device discovery overhead
.pbxproj explicitly forbiddenPrevent project corruption
Logger guidelinesDebuggable async code

Habits That Pay Off

Start each session by reading the diff. Even if you trust Claude Code, read what it changed. This is how you catch weird patterns before they spread.

Commit after each successful task. Git is your safety net. Don’t let Claude Code run for an hour before committing. Small, frequent commits mean easy rollback.

Document gotchas immediately. The moment Claude Code makes a platform-specific mistake and you fix it, add that to CLAUDE.md. Future-you will thank past-you.

Test on device, not just simulator. Claude Code can only see the simulator. Some bugs—especially around camera, GPS, and push notifications—only show up on physical hardware.

Review code for “make it work” hacks. Claude Code will sometimes take shortcuts, especially on complex tasks. Hard-coded values, commented-out code, TODO markers that never get addressed. Watch for these.

Going Deeper: Skills, Rules, and Plugins

Claude Code has a deeper customization layer I haven’t touched on here: custom slash commands, hooks that run automatically after edits, agent skills that encode domain expertise, and a growing plugin ecosystem. You can set up a /build command that knows your project’s exact scheme and simulator. You can add post-edit hooks that auto-run SwiftLint. You can create iOS-specific skills that teach Claude Code your team’s architectural patterns.

This is powerful stuff, but it’s also a rabbit hole. The basics in this article will get you productive. Once you’re comfortable with the workflow, the advanced configuration options let you remove even more friction. That’s a topic for another article.

What’s Actually Getting Better

Claude Code’s SwiftUI knowledge has improved dramatically. It handles the new Observable macro correctly. It understands Swift concurrency. It can work with Swift packages, SPM dependencies, and modular architectures.

The XcodeBuildMCP ecosystem is maturing. New versions support physical device deployment, accessibility hierarchy inspection, and UI automation. The feedback loop that used to stop at “can I build this?” now extends to “does this look right on screen?”

iOS development with Claude Code isn’t friction-free, and it might never be—Xcode is a complex beast. But with the right setup, it’s genuinely productive. The setup cost is real, but it pays back quickly.

Now stop reading and go add CLAUDE.md to your project.

Further Reading