Agent Skills: iOS App Development

Develops iOS/macOS apps with XcodeGen, SwiftUI, and SPM, including Apple Developer signing, notarization, and CI/CD pipelines. Use when building iOS/macOS apps, fixing Xcode build failures, deploying to real devices, or configuring CI/CD signing. Triggers on XcodeGen project.yml, SPM dependency issues, code signing errors (Error -25294, keychain mismatch, adhoc fallback, EMFILE, notarization credential conflict), "Library not loaded @rpath", Electron @electron/osx-sign / @electron/notarize, notarytool, or certificate/provisioning problems.

UncategorizedID: daymade/claude-code-skills/developing-ios-apps

Install this agent skill to your local

pnpm dlx add-skill https://github.com/daymade/claude-code-skills/tree/HEAD/daymade-macos/developing-ios-apps

Skill Files

Browse the full folder contents for developing-ios-apps.

Download Skill

Loading file tree…

daymade-macos/developing-ios-apps/SKILL.md

Skill Metadata

Name
developing-ios-apps
Description
Develops iOS/macOS apps with XcodeGen, SwiftUI, and SPM, including Apple Developer signing, notarization, and CI/CD pipelines. Use when building iOS/macOS apps, fixing Xcode build failures, deploying to real devices, or configuring CI/CD signing. Triggers on XcodeGen project.yml, SPM dependency issues, code signing errors (Error -25294, keychain mismatch, adhoc fallback, EMFILE, notarization credential conflict), "Library not loaded @rpath", Electron @electron/osx-sign / @electron/notarize, notarytool, or certificate/provisioning problems.

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 | | 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 | | App signed as adhoc despite certificate | @electron/packager defaults continueOnError: true | Set continueOnError: false in osxSign. See Code Signing | | "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 | | 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 |

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

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.

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

Get Team ID:

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:
deploymentTarget:
  iOS: "16.0"
  1. Fix incompatible APIs:
// 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
  1. 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)

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:

# 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

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)

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:

// 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:

// 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:

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

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:

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:

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


Resources