Skip to main content
skill CX Codex

developing-ios-apps — Claude Code skill for iOS and macOS builds

A skill for building and shipping iOS and macOS apps: XcodeGen, SwiftUI, SPM, code signing, notarization and CI. Useful when Xcode dies on signing or rpath.

Install into Claude Code · Skills CLI (Vercel) All methods and platforms ↓
npx skills add daymade/claude-code-skills --skill developing-ios-apps -a claude-code
Open on GitHub

What developing-ios-apps does

The skill covers the path from project.yml to a device and to CI. It handles XcodeGen setup, Swift Package Manager dependencies, iPhone deployment, Apple Developer signing and macOS notarization. A warning table walks through the usual failures: Library not loaded: @rpath/Framework, code-signing errors such as Error -25294, adhoc signing in @electron/packager, and EMFILE while signing a large .app bundle.

One detail breaks builds more often than anything else. XcodeGen never generates the Embed Frameworks phase for dynamic SPM frameworks, so the app compiles and then crashes at launch with image not found. The fix is manual and one-time: open the project in the Xcode GUI, go to General → Frameworks, Libraries, switch the framework to "Embed & Sign", and build from the GUI once. Command-line builds work afterwards because the setting lands in project.pbxproj. Also keep in mind that xcodegen generate wipes signing settings if they live globally — put them in the target settings instead.

Command-line builds need a paid developer account. A free Apple ID only builds from the Xcode GUI, apps stay valid for 7 days, and CI plus App Store distribution are off the table. TCC permission dialogs and disk cleanup belong to other skills.

What it helps with

  • generate an Xcode project from project.yml
  • fix a crash on Library not loaded: @rpath/Framework
  • set up signing and notarization for a macOS build
  • build an app for a connected device from the command line
  • run an iOS build inside a CI pipeline

Example prompts for the agent

  • set up XcodeGen for a new SwiftUI iOS app
  • my app crashes with Library not loaded: @rpath/RealmSwift — how do I fix it
  • build the project for my connected iPhone with xcodebuild

How to install developing-ios-apps

1Skills CLI (Vercel)

npx skills add daymade/claude-code-skills --skill developing-ios-apps -a claude-code

2Manually: put the files in place

~/.claude/skills/developing-ios-apps/SKILL.md

Copy the whole skill folder; for one project use .claude/skills/developing-ios-apps/

Step by step, with commands for each platform: How to install skills in Claude Code: five ways

Questions about developing-ios-apps

How do I install developing-ios-apps in Claude?

Run npx skills add daymade/claude-code-skills --skill developing-ios-apps -a claude-code. The skill lands in ~/.claude/skills/developing-ios-apps/SKILL.md.

What does developing-ios-apps do?

It helps you build and ship iOS and macOS apps: XcodeGen configuration, SPM dependencies, device deployment, Apple signing and notarization, and CI builds.

Why does my command-line build fail on signing?

A free Apple ID cannot build from the command line. You need a paid developer account, or build through the Xcode GUI.

Source text

SKILL.md · MIT
---
name: developing-ios-apps
description: >-
  Builds and ships iOS/macOS apps with XcodeGen, SwiftUI and SPM: Xcode build fixes, device
  deployment, Apple Developer signing/notarization, CI/CD pipelines. Use for XcodeGen project.yml
  setup, "Library not loaded @rpath" crashes, code-signing errors (Error -25294, adhoc fallback), or
  Electron notarization failures. Not for TCC permission dialogs (use macos-permissions) or disk
  cleanup (use macos-cleaner).
---

# 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

© daymade · MIT · daymade/claude-code-skills

Similar skills

Everything for Mobile →

From the same set68

skill-reviewer Reviews skill quality with design rubrics and read-only batch inventories, keeping design separate from measured task benefit and host compatibility. CX Codex +1 1,500installs mermaid-tools Pulls Mermaid blocks out of a markdown file and renders each one as a high-resolution PNG. Handy when diagrams from a README need to land in slides or a report. CX Codex +1 1,500installs excel-automation Generates styled Excel reports using openpyxl, extracts data from large xlsm models with stdlib zipfile, and drives Excel on macOS. Handy for financial modeling and rebuilding paper forms. CX Codex +1 1,500installs github-ops Operates GitHub state through gh and API calls, then reads the result back separately. Use it for pull requests, issues, repositories, and access settings. CX Codex +1 1,500installs meeting-minutes-taker Converts raw meeting transcripts into structured, evidence-based minutes, resolves anonymous speakers, and merges parallel drafts without losing details. CX Codex +1 1,500installs fact-checker The skill pulls verifiable claims out of a document, checks them against official sources and proposes corrections. Useful when versions, numbers or model specs in your text have gone stale. CX Codex +1 1,500installs promptfoo-evaluation A skill that walks the agent through setting up Promptfoo evaluations: promptfooconfig.yaml, Python assertions, and LLM-as-judge rubrics. CX Codex +1 1,500installs skills-search Searches configured local repositories before CCPM, then manages skill packages when needed. Use it to discover, inspect, install, update or remove skills. CX Codex +1 1,400installs

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor