Claude-code-skills developing-ios-apps
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.
git clone https://github.com/daymade/claude-code-skills
T=$(mktemp -d) && git clone --depth=1 https://github.com/daymade/claude-code-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/iOS-APP-developer" ~/.claude/skills/daymade-claude-code-skills-developing-ios-apps && rm -rf "$T"
iOS-APP-developer/SKILL.mdiOS 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 |
loses signing | Overwrites project settings | Configure in 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 without disabling automatic | Set first. See Camera |
| App signed as adhoc despite certificate | defaults | Set in osxSign. See Code Signing |
| "Cannot use password credentials, API key credentials..." | Passing to with API key auth | Remove . infers team from API key. See Code Signing |
| EMFILE during signing (large embedded runtime) | traverses all files in .app bundle | Add filter + in CI. See Code Signing |
Quick Reference
| Task | Command |
|---|---|
| Generate project | |
| Build simulator | |
| Build device (paid account) | |
| Clean DerivedData | |
| Find device name | |
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 |
|---|---|
| |
| Custom VStack |
| |
macro | |
| SwiftData | CoreData/Realm |
Lowering Deployment Target
- Update
:project.yml
deploymentTarget: iOS: "16.0"
- 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
- Regenerate:
xcodegen generate
Device Deployment
First-time Setup
- Connect device via USB
- Trust computer on device
- In Xcode: Settings → Accounts → Add Apple ID
- Select device in scheme dropdown
- Run (
)Cmd + R - 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: |
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
in project.yml causes build errors (XcodeGen limitation)embed: true
The Fix (Manual, one-time per project):
- Open project in Xcode GUI
- Select target → General → Frameworks, Libraries
- Find the dynamic framework (RealmSwift)
- Change "Do Not Embed" → "Embed & Sign"
- 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
- Permission: Added
to Info.plist?NSCameraUsageDescription - Device: Running on real device, not simulator?
- Session running:
called on background thread?session.startRunning() - View size: UIViewRepresentable has non-zero bounds?
- Video mirroring: Disabled
before settingautomaticallyAdjustsVideoMirroring
?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 → must choose keychain | iCloud/System → Error -25294 (private key mismatch) |
| 4 | Export P12 from keychain with password | Base64: |
| 5 | Create App Store Connect API Key (Developer role) | Download once only; record Key ID + Issuer ID |
GitHub Secrets required (5 secrets):
| Secret | Source |
|---|---|
| Step 4 base64 |
| Step 4 password |
| Step 5 base64 |
| Step 5 Key ID |
| Step 5 Issuer ID |
is NOT needed.APPLE_TEAM_IDinfers team from the API key. PassingnotarytooltoteamIdv2.5.0 causes a credential conflict error.@electron/notarize
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:
:@electron/osx-sign
— signing error throws immediatelycontinueOnError: false
hook:postPackage
+ adhoc detectioncodesign --verify --deep --strict- 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
- references/xcodegen-full.md - Complete project.yml options
- references/swiftui-compatibility.md - iOS version API differences
- references/camera-avfoundation.md - Camera preview debugging
- references/testing-mainactor.md - Testing @MainActor classes (state machines, regression tests)
- references/apple-codesign-notarize.md - Apple Developer signing + notarization for macOS/Electron CI/CD