developing-ios-apps

Universal

by daymade

Develops iOS/macOS applications with XcodeGen, SwiftUI, and SPM. Handles Apple Developer signing, notarization, and CI/CD pipelines. Triggers on XcodeGen project.yml, SPM dependency issues, device deployment, code signing errors (Error -25294, keychain mismatch, adhoc fallback, EMFILE, notarization credential conflict, continueOnError), camera/AVFoundation debugging, iOS version compatibility, "Library not loaded @rpath", Electron @electron/osx-sign/@electron/notarize config, notarytool, GitHub Actions secrets in conditionals, or certificate/provisioning problems. Use when building iOS/macOS apps, fixing Xcode build failures, deploying to real devices, or configuring CI/CD signing pipelines.

View Chinese version with editor review

安装

claude skill add --url github.com/daymade/claude-code-skills/tree/main/iOS-APP-developer

文档

iOS App Development

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

Critical Warnings

IssueCauseSolution
"Library not loaded: @rpath/Framework"XcodeGen doesn't auto-embed SPM dynamic frameworksBuild in Xcode GUI first (not xcodebuild). See Troubleshooting
xcodegen generate loses signingOverwrites project settingsConfigure in project.yml target settings, not global
Command-line signing failsFree Apple ID limitationUse Xcode GUI or paid developer account ($99/yr)
"Cannot be set when automaticallyAdjustsVideoMirroring is YES"Setting isVideoMirrored without disabling automaticSet automaticallyAdjustsVideoMirroring = false first. See Camera
App signed as adhoc despite certificate@electron/packager defaults continueOnError: trueSet continueOnError: false in osxSign. See Code Signing
"Cannot use password credentials, API key credentials..."Passing teamId to @electron/notarize with API key authRemove teamId. notarytool infers team from API key. See Code Signing
EMFILE during signing (large embedded runtime)@electron/osx-sign traverses all files in .app bundleAdd ignore filter + ulimit -n 65536 in CI. See Code Signing

Quick Reference

TaskCommand
Generate projectxcodegen generate
Build simulatorxcodebuild -destination 'platform=iOS Simulator,name=iPhone 17' build
Build device (paid account)xcodebuild -destination 'platform=iOS,name=DEVICE' -allowProvisioningUpdates build
Clean DerivedDatarm -rf ~/Library/Developer/Xcode/DerivedData/PROJECT-*
Find device namexcrun 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+ OnlyiOS 16 Compatible
.onChange { old, new in }.onChange { new in }
ContentUnavailableViewCustom VStack
AVAudioApplicationAVAudioSession
@Observable macro@ObservableObject
SwiftDataCoreData/Realm

Lowering Deployment Target

  1. Update project.yml:
yaml
deploymentTarget:
  iOS: "16.0"
  1. 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
  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)

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

Common Issues

ErrorSolution
"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 listedReconnect USB, trust computer on device, restart Xcode
DerivedData won't deleteClose Xcode first: pkill -9 Xcode && rm -rf ~/Library/Developer/Xcode/DerivedData/PROJECT-*

Free vs Paid Developer Account

FeatureFree Apple IDPaid ($99/year)
Xcode GUI builds
Command-line builds
App validity7 days1 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:

code
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

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:

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

GitHub Secrets required (5 secrets):

SecretSource
MACOS_CERT_P12Step 4 base64
MACOS_CERT_PASSWORDStep 4 password
APPLE_API_KEYStep 5 .p8 base64
APPLE_API_KEY_IDStep 5 Key ID
APPLE_API_ISSUERStep 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


Resources