Home
cd ../playbooks
Developer ToolsAdvanced

iOS App Developer

Develop iOS/macOS apps with XcodeGen, SwiftUI, and SPM — including Apple Developer signing, notarization, and CI/CD pipelines — and fix the Xcode build failures that block shipping.

20 minutes
By daymadeSource
#ios#macos#swiftui#xcode#code-signing

Code signing errors, notarization conflicts, and "Library not loaded @rpath" can stall an iOS release for days. This playbook encodes the fixes so builds and deploys actually work.

Who it's for: iOS developers, macOS developers, mobile engineers

Example

"My Xcode build fails with code signing error -25294" → Claude diagnoses the keychain mismatch and walks through the signing and notarization fix

CLAUDE.md Template

New here? 3-minute setup guide → | Already set up? Copy the template below.

# iOS App Development

Build, configure, and deploy iOS applications using XcodeGen and Swift Package Manager.

## Critical Warnings

| Issue | Cause | Solution |
|-------|-------|----------|
| "Library not loaded: @rpath/Framework" | XcodeGen doesn't auto-embed SPM dynamic frameworks | **Build in Xcode GUI first** (not xcodebuild). See [Troubleshooting](#spm-dynamic-framework-not-embedded) |
| `xcodegen generate` loses signing | Overwrites project settings | Configure in `project.yml` target settings, not global |
| Command-line signing fails | Free Apple ID limitation | Use Xcode GUI or paid developer account ($99/yr) |
| "Cannot be set when automaticallyAdjustsVideoMirroring is YES" | Setting `isVideoMirrored` without disabling automatic | Set `automaticallyAdjustsVideoMirroring = false` first. See [Camera](#camera--avfoundation) |
| App signed as adhoc despite certificate | `@electron/packager` defaults `continueOnError: true` | Set `continueOnError: false` in osxSign. See [Code Signing](#macos-code-signing--notarization) |
| "Cannot use password credentials, API key credentials..." | Passing `teamId` to `@electron/notarize` with API key auth | **Remove `teamId`**. `notarytool` infers team from API key. See [Code Signing](#macos-code-signing--notarization) |
| EMFILE during signing (large embedded runtime) | `@electron/osx-sign` traverses all files in .app bundle | Add `ignore` filter + `ulimit -n 65536` in CI. See [Code Signing](#macos-code-signing--notarization) |

## Quick Reference

| Task | Command |
|------|---------|
| Generate project | `xcodegen generate` |
| Build simulator | `xcodebuild -destination 'platform=iOS Simulator,name=iPhone 17' build` |
| Build device (paid account) | `xcodebuild -destination 'platform=iOS,name=DEVICE' -allowProvisioningUpdates build` |
| Clean DerivedData | `rm -rf ~/Library/Developer/Xcode/DerivedData/PROJECT-*` |
| Find device name | `xcrun xctrace list devices` |

## XcodeGen Configuration

### Minimal project.yml

```yaml
name: AppName
options:
  bundleIdPrefix: com.company
  deploymentTarget:
    iOS: "16.0"

settings:
  base:
    SWIFT_VERSION: "6.0"

packages:
  SomePackage:
    url: https://github.com/org/repo
    from: "1.0.0"

targets:
  AppName:
    type: application
    platform: iOS
    sources:
      - path: AppName
    settings:
      base:
        INFOPLIST_FILE: AppName/Info.plist
        PRODUCT_BUNDLE_IDENTIFIER: com.company.appname
        CODE_SIGN_STYLE: Automatic
        DEVELOPMENT_TEAM: TEAM_ID_HERE
    dependencies:
      - package: SomePackage
```

### Code Signing Configuration

**Personal (free) account**: Works in Xcode GUI only. Command-line builds require paid account.

```yaml
# In target settings
settings:
  base:
    CODE_SIGN_STYLE: Automatic
    DEVELOPMENT_TEAM: TEAM_ID  # Get from Xcode → Settings → Accounts
```

**Get Team ID**:
```bash
security find-identity -v -p codesigning | head -3
```

## iOS Version Compatibility

### API Changes by Version

| iOS 17+ Only | iOS 16 Compatible |
|--------------|-------------------|
| `.onChange { old, new in }` | `.onChange { new in }` |
| `ContentUnavailableView` | Custom VStack |
| `AVAudioApplication` | `AVAudioSession` |
| `@Observable` macro | `@ObservableObject` |
| SwiftData | CoreData/Realm |

### Lowering Deployment Target

1. Update `project.yml`:
```yaml
deploymentTarget:
  iOS: "16.0"
```

2. Fix incompatible APIs:
```swift
// iOS 17
.onChange(of: value) { oldValue, newValue in }
// iOS 16
.onChange(of: value) { newValue in }

// iOS 17
ContentUnavailableView("Title", systemImage: "icon")
// iOS 16
VStack {
    Image(systemName: "icon").font(.system(size: 48))
    Text("Title").font(.title2.bold())
}

// iOS 17
AVAudioApplication.shared.recordPermission
// iOS 16
AVAudioSession.sharedInstance().recordPermission
```

3. Regenerate: `xcodegen generate`

## Device Deployment

### First-time Setup

1. Connect device via USB
2. Trust computer on device
3. In Xcode: Settings → Accounts → Add Apple ID
4. Select device in scheme dropdown
5. Run (`Cmd + R`)
6. On device: Settings → General → VPN & Device Management → Trust

### Command-line Build (requires paid account)

```bash
xcodebuild \
  -project App.xcodeproj \
  -scheme App \
  -destination 'platform=iOS,name=DeviceName' \
  -allowProvisioningUpdates \
  build
```

### Common Issues

| Error | Solution |
|-------|----------|
| "Library not loaded: @rpath/Framework" | SPM dynamic framework not embedded. Build in Xcode GUI first, then CLI works |
| "No Account for Team" | Add Apple ID in Xcode Settings → Accounts |
| "Provisioning profile not found" | Free account limitation. Use Xcode GUI or get paid account |
| Device not listed | Reconnect USB, trust computer on device, restart Xcode |
| DerivedData won't delete | Close Xcode first: `pkill -9 Xcode && rm -rf ~/Library/Developer/Xcode/DerivedData/PROJECT-*` |

### Free vs Paid Developer Account

| Feature | Free Apple ID | Paid ($99/year) |
|---------|---------------|-----------------|
| Xcode GUI builds | ✅ | ✅ |
| Command-line builds | ❌ | ✅ |
| App validity | 7 days | 1 year |
| App Store | ❌ | ✅ |
| CI/CD | ❌ | ✅ |

## SPM Dependencies

### SPM Dynamic Framework Not Embedded

**Root Cause**: XcodeGen doesn't generate the "Embed Frameworks" build phase for SPM dynamic frameworks (like RealmSwift, Realm). The app builds successfully but crashes on launch with:

```
dyld: Library not loaded: @rpath/RealmSwift.framework/RealmSwift
  Referenced from: /var/containers/Bundle/Application/.../App.app/App
  Reason: image not found
```

**Why This Happens**:
- Static frameworks (most SPM packages) are linked into the binary - no embedding needed
- Dynamic frameworks (RealmSwift, etc.) must be copied into the app bundle
- XcodeGen generates link phase but NOT embed phase for SPM packages
- `embed: true` in project.yml causes build errors (XcodeGen limitation)

**The Fix** (Manual, one-time per project):
1. Open project in Xcode GUI
2. Select target → General → Frameworks, Libraries
3. Find the dynamic framework (RealmSwift)
4. Change "Do Not Embed" → "Embed & Sign"
5. Build and run from Xcode GUI first

**After Manual Fix**: Command-line builds (`xcodebuild`) will work because Xcode persists the embed setting in project.pbxproj.

**Identifying Dynamic Frameworks**:
```bash
# Check if a framework is dynamic
file ~/Library/Developer/Xcode/DerivedData/PROJECT-*/Build/Products/Debug-iphoneos/FRAMEWORK.framework/FRAMEWORK
# Dynamic: "Mach-O 64-bit dynamically linked shared library"
# Static: "current ar archive"
```

### Adding Packages

```yaml
packages:
  AudioKit:
    url: https://github.com/AudioKit/AudioKit
    from: "5.6.5"
  RealmSwift:
    url: https://github.com/realm/realm-swift
    from: "10.54.6"

targets:
  App:
    dependencies:
      - package: AudioKit
      - package: RealmSwift
        product: RealmSwift  # Explicit product name when package has multiple
```

### Resolving Dependencies (China proxy)

```bash
git config --global http.proxy http://127.0.0.1:1082
git config --global https.proxy http://127.0.0.1:1082
xcodebuild -scmProvider system -resolvePackageDependencies
```

**Never clear global SPM cache** (`~/Library/Caches/org.swift.swiftpm`). Re-downloading is slow.

## Camera / AVFoundation

Camera preview requires real device (simulator has no camera).

### Quick Debugging Checklist

1. **Permission**: Added `NSCameraUsageDescription` to Info.plist?
2. **Device**: Running on real device, not simulator?
3. **Session running**: `session.startRunning()` called on background thread?
4. **View size**: UIViewRepresentable has non-zero bounds?
5. **Video mirroring**: Disabled `automaticallyAdjustsVideoMirroring` before setting `isVideoMirrored`?

### Video Mirroring (Front Camera)

**CRITICAL**: Must disable automatic adjustment before setting manual mirroring:

```swift
// WRONG - crashes with "Cannot be set when automaticallyAdjustsVideoMirroring is YES"
connection.isVideoMirrored = true

// CORRECT - disable automatic first
connection.automaticallyAdjustsVideoMirroring = false
connection.isVideoMirrored = true
```

### UIViewRepresentable Sizing Issue

UIViewRepresentable in ZStack may have zero bounds. Fix with explicit frame:

```swift
// BAD: UIViewRepresentable may get zero size in ZStack
ZStack {
    CameraPreviewView(session: session)  // May be invisible!
    OtherContent()
}

// GOOD: Explicit sizing
ZStack {
    GeometryReader { geo in
        CameraPreviewView(session: session)
            .frame(width: geo.size.width, height: geo.size.height)
    }
    .ignoresSafeArea()
    OtherContent()
}
```

### Debug Logging Pattern

Add logging to trace camera flow:

```swift
import os
private let logger = Logger(subsystem: "com.app", category: "Camera")

func start() async {
    logger.info("start() called, isRunning=\(self.isRunning)")
    // ... setup code ...
    logger.info("session.startRunning() completed")
}

// For CGRect (doesn't conform to CustomStringConvertible)
logger.info("bounds=\(NSCoder.string(for: self.bounds))")
```

Filter in Console.app by subsystem.

**For detailed camera implementation**: See [references/camera-avfoundation.md](references/camera-avfoundation.md)

## macOS Code Signing & Notarization

For distributing macOS apps (Electron or native) outside the App Store, signing + notarization is required. Without it users see "Apple cannot check this app for malicious software."

**5-step checklist:**

| Step | What | Critical detail |
|------|------|-----------------|
| 1 | Create CSR in Keychain Access | Common Name doesn't matter; choose "Saved to disk" |
| 2 | Request **Developer ID Application** cert at developer.apple.com | Choose **G2 Sub-CA** (not Previous Sub-CA) |
| 3 | Install `.cer` → must choose **`login` keychain** | iCloud/System → Error -25294 (private key mismatch) |
| 4 | Export P12 from `login` keychain with password | Base64: `base64 -i cert.p12 \| pbcopy` |
| 5 | Create App Store Connect API Key (Developer role) | Download `.p8` once only; record Key ID + Issuer ID |

**GitHub Secrets required (5 secrets):**

| Secret | Source |
|--------|--------|
| `MACOS_CERT_P12` | Step 4 base64 |
| `MACOS_CERT_PASSWORD` | Step 4 password |
| `APPLE_API_KEY` | Step 5 `.p8` base64 |
| `APPLE_API_KEY_ID` | Step 5 Key ID |
| `APPLE_API_ISSUER` | Step 5 Issuer ID |

> **`APPLE_TEAM_ID` is NOT needed.** `notarytool` infers team from the API key. Passing `teamId` to `@electron/notarize` v2.5.0 causes a credential conflict error.

**Electron Forge osxSign critical settings:**

```typescript
osxSign: {
  identity: 'Developer ID Application',
  hardenedRuntime: true,
  entitlements: 'entitlements.mac.plist',
  entitlementsInherit: 'entitlements.mac.plist',
  continueOnError: false,  // CRITICAL: default is true, silently falls back to adhoc
  // Skip non-binary files in large embedded runtimes (prevents EMFILE)
  ignore: (filePath: string) => {
    if (!filePath.includes('python-runtime')) return false;
    if (/\.(so|dylib|node)$/.test(filePath)) return false;
    return true;
  },
  // CI: explicitly specify keychain (apple-actions/import-codesign-certs uses signing_temp.keychain)
  ...(process.env.MACOS_SIGNING_KEYCHAIN
    ? { keychain: process.env.MACOS_SIGNING_KEYCHAIN }
    : {}),
},
```

**Fail-fast three-layer defense:**

1. `@electron/osx-sign`: `continueOnError: false` — signing error throws immediately
2. `postPackage` hook: `codesign --verify --deep --strict` + adhoc detection
3. Release trigger script: verify local HEAD matches remote before dispatch

**Verify signing:**
```bash
security find-identity -v -p codesigning | grep "Developer ID Application"
```

For complete step-by-step guide, entitlements, workflow examples, and full troubleshooting (7 real-world errors with root causes): **[references/apple-codesign-notarize.md](references/apple-codesign-notarize.md)**

---

## Resources

- [references/xcodegen-full.md](references/xcodegen-full.md) - Complete project.yml options
- [references/swiftui-compatibility.md](references/swiftui-compatibility.md) - iOS version API differences
- [references/camera-avfoundation.md](references/camera-avfoundation.md) - Camera preview debugging
- [references/testing-mainactor.md](references/testing-mainactor.md) - Testing @MainActor classes (state machines, regression tests)
- [references/apple-codesign-notarize.md](references/apple-codesign-notarize.md) - Apple Developer signing + notarization for macOS/Electron CI/CD

Get new playbooks like this one

One email a week with new Claude Code workflows. Free, like everything here.

No spam. Unsubscribe anytime.

README.md

What This Does

Develop iOS/macOS apps with XcodeGen, SwiftUI, and SPM — including Apple Developer signing, notarization, and CI/CD pipelines — and fix the Xcode build failures that block shipping.

What's Inside

The template covers:

  • Critical Warnings
  • Quick Reference
  • XcodeGen Configuration
  • iOS Version Compatibility
  • Device Deployment
  • SPM Dependencies

Quick Start

Step 1: Create a Project Folder

Make a dedicated folder for this workflow and open it in Claude Code.

Step 2: Download the Template

Click Download above to save the template, then drop it into your project as CLAUDE.md (or paste it into your existing one).

Step 3: Start Working

Tell Claude what you need in plain language — it will follow the template's workflow automatically. For example:

My Xcode build fails with code signing error -25294

Claude reads the template and runs the steps for you.

$Related Playbooks

Developer Tools

LangSmith Fetch

Debug LangChain and LangGraph agents by fetching execution traces from LangSmith Studio for error analysis and performance optimization.

10 minutes
Intermediate
Developer Tools

Long-Horizon Agent Prompting

Design the launch prompt for an agent working autonomously for hours or days — a pseudo-formal task brief with an exact success predicate, an enumerated non-counting-outcomes list, adversarial verification, and effort floors, because persistence pressure against a loose spec produces confident non-solutions.

10 minutes
Advanced
Developer Tools

Loopy: Build and Reuse Agent Loops

Mine your codebase and coding history for repeated work, turn it into bounded agent loops, and audit the loops you already have

10 minutes
Intermediate
Developer Tools

MCP Server Hub Manager

Access 1200+ AI Agent tools via Model Context Protocol (MCP)

10 minutes
Advanced
Developer Tools

MCP Server Builder

Guide for creating high-quality Model Context Protocol (MCP) servers that enable LLMs to interact with external services and APIs.

30 minutes
Advanced
Developer Tools

Minimalist UI Design Skill: Editorial, Monochrome Interfaces

A frontend design system that enforces warm monochrome, bento grids, and editorial typography — the Notion/Linear look, banned from gradients, heavy shadows, and Inter

5 minutes
Beginner
Developer Tools

Mermaid Tools

Extract Mermaid diagrams from markdown files and render them to high-quality PNG images using bundled scripts — turning text diagrams into shareable visuals.

5 minutes
Beginner
Developer Tools

Minimalist MVP Scoping

Scope a weekend-shippable MVP using the manual → processized → productized progression. Build as little as possible; charge from day one.

5 minutes
Beginner
Developer Tools

n8n Workflow Builder

Automate document workflows with n8n - 7800+ workflow templates

10 minutes
Advanced
Developer Tools

Planning with Files

Persistent, file-based planning for multi-step AI-agent work — task_plan.md, findings.md, and progress.md on disk, a 2-action rule for capturing multimodal findings before they're lost, a 3-strike error protocol, and a 5-question reboot test to verify state survives a compaction.

5 minutes
Intermediate
Developer Tools

PR Queue Triage

Clear a backlog of open pull requests before a release by classifying every PR into an evidence-based disposition — never by title — with a real git merge-tree test against the actual release branch, not the platform's often-wrong mergeable flag.

10 minutes
Intermediate
Developer Tools

Reproducible Database Lookup

A methodology for querying public database APIs — scientific, regulatory, financial, or otherwise — so another agent or human can repeat exactly what you did: bounded calls, count reconciliation, identifier-conversion tracking, and untrusted-data handling for every response.

10 minutes
Advanced

Browse all Developer Tools playbooks →