Claude Code for Swift & iOS — Complete Setup & Workflow Guide
Claude Code integrates powerfully with Swift development when configured for your Apple platform stack. Since Claude Code runs in the terminal alongside Xcode, this guide covers the CLAUDE.md template for iOS/macOS projects, automated xcodebuild and swift test hooks, SwiftUI workflow patterns, Swift Package Manager setup, and iOS-specific workflows for networking, persistence, and background tasks.
Swift / iOS CLAUDE.md Template
# Project: [Your iOS App / Swift Package Name]
## Platform
- iOS 17+ / macOS 14+ (Swift 5.9)
- UI Framework: SwiftUI with @Observable macro (or UIKit + Combine)
- Architecture: MVVM (ViewModel: @Observable class, View: SwiftUI struct)
- Package manager: Swift Package Manager (or CocoaPods — specify)
## Key commands
```bash
swift build # build Swift package (libraries)
swift test # run all tests (Swift package)
xcodebuild test \
-scheme YourApp \
-destination 'platform=iOS Simulator,name=iPhone 16' \
-quiet 2>&1 | tail -30 # Xcode project tests
xcodebuild build \
-scheme YourApp \
-destination 'platform=iOS Simulator,name=iPhone 16' \
-quiet # build check
xcrun simctl list devices available # list available simulators
```
## Stack
- Networking: URLSession with async/await (no Alamofire unless specified)
- Persistence: SwiftData (iOS 17+) with @Model and ModelContainer
- State management: @Observable (Swift 5.9+), @State, @Binding, @Environment
- Concurrency: structured concurrency — async/await, Task, TaskGroup, actor
- Image loading: AsyncImage (SwiftUI built-in)
- Auth: Sign in with Apple + local keychain via Security framework
## Conventions
- Views: pure SwiftUI structs; no business logic in View body
- ViewModels: @Observable class; never import SwiftUI in ViewModel
- Error handling: Result<T, Error> for API responses; never force-unwrap
- Async: use async throws functions, propagate errors up to the ViewModel
- Actors: use actor for shared mutable state (e.g. cache, token store)
- Previews: every new View gets a #Preview block with realistic sample data
## Project structure
- Sources/
- App/ — @main App struct, scene setup
- Features/ — feature folders (each: View + ViewModel)
- Models/ — SwiftData @Model types
- Services/ — networking, persistence, auth services
- Extensions/ — Swift extension helpers
- Tests/ — XCTest unit + integration tests
- UITests/ — XCUITest UI test targets
Automated xcodebuild / swift test Hooks
Run tests after every edit so Claude sees failures immediately and iterates to fix them:
// .claude/settings.json (Swift Package Manager project)
{
"allowedTools": ["Edit", "Write", "Bash", "Read", "Glob", "Grep"],
"hooks": [
{
"event": "PostToolUse",
"matcher": "Edit|Write",
"hooks": [{
"type": "command",
"command": "cd $PROJECT_ROOT && swift build 2>&1 | tail -20"
}]
}
]
}
// .claude/settings.json (Xcode project)
{
"hooks": [
{
"event": "PostToolUse",
"matcher": "Edit|Write",
"hooks": [{
"type": "command",
"command": "xcodebuild test -scheme YourApp -destination 'platform=iOS Simulator,name=iPhone 16' -quiet 2>&1 | grep -E '(error:|warning:|FAILED|passed|failed)' | tail -20"
}]
}
]
}
Use
swift test --filter TestSuiteName/testMethodName to run a single test during TDD. Add -parallel for faster parallel test execution on large test suites.SwiftLint hook (catch style issues automatically)
{
"event": "PostToolUse",
"matcher": "Write",
"hooks": [{
"type": "command",
"command": "cd $PROJECT_ROOT && swiftlint lint --quiet 2>&1 | head -30"
}]
}
SwiftUI Workflow Patterns
Tell Claude your SwiftUI conventions up front to get consistent, idiomatic output:
| Pattern | CLAUDE.md instruction |
|---|---|
| State management | "Use @Observable for ViewModels (Swift 5.9); @State for local view state only" |
| Navigation | "Use NavigationStack with navigationDestination(for:) and a NavigationPath in the ViewModel" |
| Async data loading | "Load data in .task { } modifier; set @Observable var isLoading = true during fetch" |
| Error presentation | "Present errors via .alert(isPresented:) bound to ViewModel.errorMessage" |
| List performance | "Use LazyVStack inside ScrollView for heterogeneous lists; List for homogeneous data" |
| Previews | "Every View file gets a #Preview block with @Previewable @State for interactive mocks" |
New SwiftUI screen (feature + ViewModel)
claude "add a UserProfileView screen.
ViewModel: UserProfileViewModel (@Observable class).
Fetch user data from UserService.fetchUser(id:) on appear.
Display: avatar (AsyncImage), name, bio, follower count.
Navigation: tap avatar opens FullScreenCover with zoomable image.
Add #Preview with a mock UserService.
Put View in Features/UserProfile/UserProfileView.swift,
ViewModel in Features/UserProfile/UserProfileViewModel.swift."
SwiftData model + repository
claude "add a CachedArticle SwiftData model.
Fields: id (UUID), title, body, publishedAt (Date), isFavorite (Bool).
Create ArticleRepository actor that wraps ModelContext for thread-safe access.
Methods: save(article:), fetchAll() -> [CachedArticle], delete(id:).
Inject ModelContainer via environment; use @Query in the list View.
Write XCTest tests for ArticleRepository using an in-memory ModelContainer."
Networking with async/await
# CLAUDE.md additions for networking
## Networking conventions
- HTTP client: URLSession with async/await (no third-party unless in dependencies)
- Base URL and auth headers in APIClient actor (singleton via dependency injection)
- All endpoints: async throws returning Decodable models
- Error type: APIError enum (unauthorized, notFound, serverError(Int), decodingError)
- Retry: 3 attempts with exponential backoff on 5xx using Task.sleep(nanoseconds:)
- Logging: os.log(.debug) for requests; never log auth tokens
claude "add a NewsAPIClient that fetches /api/v1/articles.
Endpoint returns [ArticleResponse] — define the Decodable struct.
Handle 401 by calling AuthService.refreshToken() and retrying once.
Add caching: store last response in UserDefaults with a 5-minute TTL.
Write XCTest unit tests using a URLProtocol mock."
Swift Package Manager Workflows
# CLAUDE.md for Swift Package (library)
## Package setup
- Swift tools version: 5.10
- Platforms: .iOS(.v17), .macOS(.v14), .watchOS(.v10), .tvOS(.v17)
- Targets: Sources/MyLib (library), Tests/MyLibTests (test target)
- No UI dependencies in library target (UI in example app only)
## Commands
swift build --configuration release # release build
swift test --enable-code-coverage # tests + coverage
swift package generate-xcodeproj # generate Xcode project (if needed)
claude "add a KeychainStore struct to Sources/MyLib.
Generic API: func save<T: Codable>(_ value: T, key: String) throws.
func load<T: Codable>(_ type: T.Type, key: String) throws -> T.
Use Security framework (kSecClassGenericPassword).
Write XCTest tests that save, load, and delete values.
Do not add any external dependencies."
Swift Concurrency Patterns
| Pattern | Claude Code prompt angle |
|---|---|
| Parallel API calls | "Fetch user, posts, and followers concurrently using async let; merge results into a UserFeed model" |
| Shared mutable cache | "Create an ImageCache actor with get(url:) and set(url:image:); use it from AsyncImage replacement" |
| Background processing | "Add a BackgroundSyncService actor that runs every 30 min; cancel via CancellationToken when app enters background" |
| MainActor UI updates | "All ViewModel @Published properties annotated @MainActor; network calls on background task" |
| Combine bridge | "Wrap URLSession.dataTaskPublisher in async/await using withCheckedThrowingContinuation for legacy code" |
5 Tips for Swift + Claude Code
- Paste your
Package.swiftorPodfilecontents into CLAUDE.md under a "Dependencies" section. Claude will use your exact library versions and APIs, not guess at them. - Specify your minimum OS version explicitly: "Minimum deployment: iOS 17". Claude will then use @Observable, SwiftData, and TipKit instead of older APIs, and won't generate deprecated Combine-heavy code.
- For Xcode projects, commit a minimal
project.pbxprojbefore starting a Claude Code session. New files Claude creates won't auto-add to the Xcode target — you'll need to drag them in or usetuist/XcodeGento regenerate the project file. - Tell Claude your SwiftData model version if you have migrations: "SwiftData schema version: 2; VersionedSchema in Models/MigrationPlan.swift". Claude will respect the schema versioning constraints and not break migrations.
- Use
swift test --filterwith a PostToolUse hook during TDD to get sub-500ms feedback on a single test. Switch to the full suite before finishing a feature.