
Developing Ios Apps
- 968 installs
- 1.3k repo stars
- Updated August 4, 2026
- daymade/claude-code-skills
developing-ios-apps is a Claude Code skill that scaffolds, debugs, signs, and deploys iOS and macOS apps with XcodeGen, SwiftUI, and Swift Package Manager for developers shipping Apple platform software.
About
developing-ios-apps is a Claude Code skill from daymade/claude-code-skills for end-to-end Apple platform development. It covers XcodeGen project.yml generation, SwiftUI interfaces, Swift Package Manager dependency resolution, Apple Developer code signing, notarization with notarytool, and GitHub Actions CI/CD including secrets in conditionals. The skill triggers on concrete failure modes: Error -25294, keychain mismatch, adhoc fallback, EMFILE, notarization credential conflicts, continueOnError, Library not loaded @rpath, iOS version compatibility, and Electron @electron/osx-sign/@electron/notarize configuration. Developers reach for it when building for real devices, fixing Xcode build failures, or wiring certificate and provisioning workflows that block release.
- Handles XcodeGen project.yml configuration and SPM dependency resolution
- Resolves common code signing, notarization, and provisioning errors including Error -25294 and keychain mismatches
- Manages Apple Developer account workflows, device deployment, and CI/CD pipeline secrets
- Debugs AVFoundation camera issues, dynamic framework embedding, and library loading failures
- Supports Electron macOS signing/notarization and GitHub Actions conditional secrets
Developing Ios Apps by the numbers
- 968 all-time installs (skills.sh)
- +52 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #242 of 1,039 Mobile Development skills by installs in the Skillselion catalog
- Security screen: CRITICAL risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/daymade/claude-code-skills --skill developing-ios-appsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 968 |
|---|---|
| repo stars | ★ 1.3k |
| Security audit | 2 / 3 scanners passed |
| Last updated | August 4, 2026 |
| Repository | daymade/claude-code-skills ↗ |
How do you fix Xcode code signing and notarization errors?
Generate, configure, debug, and deploy iOS and macOS apps using XcodeGen, SwiftUI, and Swift Package Manager.
Who is it for?
iOS and macOS developers debugging XcodeGen, SwiftUI, SPM, provisioning profiles, or notarization in CI.
Skip if: Android, Flutter-only, or web-only projects with no Apple toolchain or signing requirements.
When should I use this skill?
The user mentions XcodeGen, SwiftUI, SPM errors, code signing -25294, notarytool, device deployment, or @electron/osx-sign configuration.
What you get
Working XcodeGen project.yml, signed iOS/macOS binaries, resolved SPM graph, and passing GitHub Actions deploy pipeline.
- signed app binary
- XcodeGen project.yml
- CI/CD workflow configuration
Files
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: SomePackageCode 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 → AccountsGet Team ID:
security find-identity -v -p codesigning | head -3iOS 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"2. 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().recordPermission3. 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 \
buildCommon 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 foundWhy 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: truein 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 multipleResolving 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 -resolvePackageDependenciesNever 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 = trueUIViewRepresentable 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 \ |
| 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.notarytoolinfers team from the API key. PassingteamIdto@electron/notarizev2.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](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
Security scan passed
Scanned at: 2025-12-15T22:23:52.306779
Tool: gitleaks + pattern-based validation
Content hash: 1e3661252b87a1effd6b4e458364ab4e7b8bd6356360aea058593e70ed0edd11
Apple Code Signing + Notarization Guide
For macOS desktop apps (Electron or native) distributed outside the App Store. Without signing + notarization, users see "Apple cannot check this app for malicious software."
---
Prerequisites
- Apple Developer Program ($99/year)
- Record Team ID (developer.apple.com → Account → Membership Details)
---
Step 1: Create Developer ID Application Certificate
Developer ID Application = distribution outside App Store (DMG/ZIP).
Mac App Distribution = App Store only.
1a. Generate CSR
Keychain Access → Certificate Assistant → Request a Certificate from a Certificate Authority:
| Field | Value |
|---|---|
| User Email Address | Apple Developer email |
| Common Name | Anything (Apple overrides this) |
| CA Email Address | Leave empty |
| Request is | Saved to disk |
1b. Request Certificate
1. Go to developer.apple.com/account/resources/certificates/add 2. Select Developer ID Application 3. Choose G2 Sub-CA (Xcode 11.4.1 or later) (not Previous Sub-CA) 4. Upload CSR, download .cer
1c. Install to Keychain
Double-click .cer → must choose `login` keychain. Choosing iCloud/System causes Error -25294 (private key not in same keychain).
1d. Verify
security find-identity -v -p codesigning | grep "Developer ID Application"---
Step 2: Export P12 (for CI)
1. Keychain Access → My Certificates → find Developer ID Application: ... 2. Right-click → Export → .p12 format → set strong password 3. Convert to base64:
base64 -i ~/Desktop/codesign.p12 | pbcopy---
Step 3: Create App Store Connect API Key (for notarization)
API Key avoids 2FA prompts in CI. Apple's recommended approach for automation.
1. Go to appstoreconnect.apple.com/access/integrations/api 2. Generate API Key (Access: Developer) 3. Download .p8 (one-time only) 4. Record Key ID (10 chars) and Issuer ID (UUID)
base64 -i ~/Downloads/AuthKey_KEYID.p8 | pbcopy---
Step 4: Configure GitHub Secrets
5 secrets required (secret names must exactly match workflow references):
| Secret | Source |
|---|---|
MACOS_CERT_P12 | Step 2 base64 |
MACOS_CERT_PASSWORD | Step 2 password |
APPLE_API_KEY | Step 3 .p8 base64 |
APPLE_API_KEY_ID | Step 3 Key ID |
APPLE_API_ISSUER | Step 3 Issuer ID |
`APPLE_TEAM_ID` is NOT needed and MUST NOT be passed.@electron/notarizev2.5.0'sisNotaryToolPasswordCredentials()checksteamId !== undefined. PassingteamIdalongside API key credentials triggers: "Cannot use password credentials, API key credentials and keychain credentials at once."notarytoolinfers team from the API key automatically.
Setting secrets via gh CLI
# Short values
gh secret set MACOS_CERT_PASSWORD --body 'your-password' --repo owner/repo
gh secret set APPLE_API_KEY_ID --body 'KEYIDHERE' --repo owner/repo
gh secret set APPLE_API_ISSUER --body 'uuid-here' --repo owner/repo
# Long base64 values — use temp file to avoid zsh glob expansion errors
printf '%s' '<base64>' > /tmp/p12.txt
gh secret set MACOS_CERT_P12 < /tmp/p12.txt --repo owner/repo && rm /tmp/p12.txt
printf '%s' '<base64>' > /tmp/apikey.txt
gh secret set APPLE_API_KEY < /tmp/apikey.txt --repo owner/repo && rm /tmp/apikey.txtDual-repo architecture: If using private dev repo + public release repo, set secrets on both repos separately.
Verify
gh secret list --repo owner/repo---
Step 5: Electron Forge Configuration
osxSign (signing)
const SHOULD_CODESIGN = process.env.FLOWZERO_CODESIGN === '1';
const SHOULD_NOTARIZE = process.env.FLOWZERO_NOTARIZE === '1';
const CODESIGN_IDENTITY = process.env.CODESIGN_IDENTITY || 'Developer ID Application';
// In packagerConfig:
...(SHOULD_CODESIGN ? {
osxSign: {
identity: CODESIGN_IDENTITY,
hardenedRuntime: true,
entitlements: 'entitlements.mac.plist',
entitlementsInherit: 'entitlements.mac.plist',
// CRITICAL: @electron/packager defaults continueOnError to true,
// which silently swallows ALL signing failures and falls back to adhoc.
continueOnError: false,
// Skip non-binary files in large embedded runtimes (e.g. Python).
// Without this, osx-sign traverses 50k+ files → EMFILE errors.
// Native .so/.dylib/.node binaries are still signed.
ignore: (filePath: string) => {
if (!filePath.includes('python-runtime')) return false;
if (/\.(so|dylib|node)$/.test(filePath)) return false;
return true;
},
// CI: apple-actions/import-codesign-certs@v3 imports to signing_temp.keychain,
// but @electron/osx-sign searches system keychain by default.
...(process.env.MACOS_SIGNING_KEYCHAIN
? { keychain: process.env.MACOS_SIGNING_KEYCHAIN }
: {}),
},
} : {}),osxNotarize (notarization)
...(SHOULD_NOTARIZE ? {
osxNotarize: {
tool: 'notarytool',
appleApiKey: process.env.APPLE_API_KEY_PATH,
appleApiKeyId: process.env.APPLE_API_KEY_ID,
appleApiIssuer: process.env.APPLE_API_ISSUER,
// NOTE: Do NOT pass teamId. See Step 4 explanation above.
},
} : {}),postPackage Fail-Fast Verification
Add codesign --verify --deep --strict + adhoc detection in the postPackage hook:
import { execSync } from 'child_process';
// In postPackage hook:
if (SHOULD_CODESIGN && process.platform === 'darwin') {
const appDir = fs.readdirSync(buildPath).find(e => e.endsWith('.app'));
if (!appDir) throw new Error('CODESIGN FAIL-FAST: No .app bundle found');
const appPath = path.join(buildPath, appDir);
// 1. Verify signature is valid
try {
execSync(`codesign --verify --deep --strict "${appPath}"`, { stdio: 'pipe' });
} catch (e) {
const stderr = (e as { stderr?: Buffer })?.stderr?.toString() || '';
throw new Error(`CODESIGN FAIL-FAST: Verification failed.\n ${stderr}`);
}
// 2. Check it's NOT adhoc
const info = execSync(`codesign -dvv "${appPath}" 2>&1`, { encoding: 'utf-8' });
if (info.includes('Signature=adhoc')) {
throw new Error('CODESIGN FAIL-FAST: App has adhoc signature! Signing silently failed.');
}
const authority = info.match(/Authority=(.+)/);
if (authority) console.log(`Signed by: ${authority[1]}`);
}entitlements.mac.plist (Electron + Python)
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<false/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.device.microphone</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>---
Step 6: GitHub Actions Workflow
Key pattern: secrets in step if: conditions
secrets.* context cannot be used directly in step if:. Use env: intermediate variables:
# WRONG — causes HTTP 422: "Unrecognized named-value: 'secrets'"
- name: Import certs
if: ${{ secrets.MACOS_CERT_P12 != '' }}
# CORRECT — use env: intermediate variable
- name: Import certs
if: ${{ env.HAS_CERT == 'true' }}
env:
HAS_CERT: ${{ secrets.MACOS_CERT_P12 != '' }}Complete workflow example
- name: Import Apple certificates
if: ${{ env.HAS_CERT == 'true' }}
uses: apple-actions/import-codesign-certs@v3
with:
p12-file-base64: ${{ secrets.MACOS_CERT_P12 }}
p12-password: ${{ secrets.MACOS_CERT_PASSWORD }}
env:
HAS_CERT: ${{ secrets.MACOS_CERT_P12 != '' }}
- name: Verify signing identity
if: ${{ env.HAS_CERT == 'true' }}
run: security find-identity -v -p codesigning | grep "Developer ID"
env:
HAS_CERT: ${{ secrets.MACOS_CERT_P12 != '' }}
- name: Prepare API key
if: ${{ env.HAS_API_KEY == 'true' }}
run: |
set -euo pipefail
if [[ "$APPLE_API_KEY" == *"BEGIN PRIVATE KEY"* ]]; then
printf "%s" "$APPLE_API_KEY" > /tmp/AuthKey.p8
else
echo "$APPLE_API_KEY" | base64 --decode > /tmp/AuthKey.p8
fi
env:
HAS_API_KEY: ${{ secrets.APPLE_API_KEY != '' }}
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
- name: Build & sign
env:
FLOWZERO_CODESIGN: ${{ secrets.MACOS_CERT_P12 != '' && '1' || '' }}
FLOWZERO_NOTARIZE: ${{ secrets.APPLE_API_KEY != '' && '1' || '' }}
APPLE_API_KEY_PATH: /tmp/AuthKey.p8
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
# NOTE: APPLE_TEAM_ID intentionally omitted — notarytool infers from API key
run: |
ulimit -n 65536 # Prevent EMFILE when signing large app bundles
pnpm run forge:make -- --arch=arm64---
Fail-Fast Three-Layer Defense
Signing can fail silently in many ways. This architecture ensures any failure is caught immediately:
| Layer | Mechanism | What it catches |
|---|---|---|
1. @electron/osx-sign | continueOnError: false | Signing errors (EMFILE, cert not found, timestamp failures) |
2. postPackage hook | codesign --verify --deep --strict + adhoc detection | Silent signing failures, unexpected adhoc fallback |
| 3. Release trigger | Verify local HEAD matches remote branch | Stale code reaching CI (SHA vs branch name issue) |
Release trigger script pattern
# Send branch name, NOT commit SHA
BRANCH=$(git rev-parse --abbrev-ref HEAD)
LOCAL_HEAD=$(git rev-parse HEAD)
REMOTE_HEAD=$(git ls-remote origin "$BRANCH" 2>/dev/null | awk '{print $1}')
if [[ "$LOCAL_HEAD" != "$REMOTE_HEAD" ]]; then
echo "FAIL-FAST: Local HEAD does not match remote!"
echo " Local: $LOCAL_HEAD"
echo " Remote: $REMOTE_HEAD"
echo " Push first: git push origin $BRANCH"
exit 1
fi
# Dispatch with branch name (not SHA)
gh api repos/OWNER/REPO/dispatches -f event_type=release -f 'client_payload[ref]='"$BRANCH"Why branch name, not SHA:actions/checkoutusesrefs/heads/<ref>*glob matching for shallow clones. Commit SHAs don't match this pattern and cause checkout failure.
---
Troubleshooting
| Symptom | Root Cause | Fix |
|---|---|---|
| App signed as adhoc despite certificate configured | @electron/packager defaults continueOnError: true in createSignOpts() (mac.js line 402-404). Signing error was silently swallowed. | Set continueOnError: false in osxSign config |
| "Cannot use password credentials, API key credentials and keychain credentials at once" | @electron/notarize v2.5.0 isNotaryToolPasswordCredentials() checks teamId !== undefined. Passing teamId with API key = credential conflict. | Remove teamId from osxNotarize config. notarytool infers team from API key. |
| EMFILE: too many open files | @electron/osx-sign walkAsync() traverses ALL files in .app. Large embedded runtimes (Python: 51k+ files) exhaust file descriptors. | Add ignore filter to skip non-binary files + ulimit -n 65536 in CI |
| CI signing: cert not found | apple-actions/import-codesign-certs@v3 imports to signing_temp.keychain, but osx-sign searches system keychain. | Pass keychain: process.env.MACOS_SIGNING_KEYCHAIN in osxSign |
| Install .cer: Error -25294 | Certificate imported to wrong keychain (iCloud/System). Private key from CSR is in login keychain. | Re-import .cer choosing login keychain |
security find-identity shows nothing | Private key and certificate in different keychains | Ensure CSR private key and imported cert are both in login keychain |
CI step if: with secrets → HTTP 422 | secrets.* context not available in step if: conditions | Use env: intermediate variable pattern (see workflow section) |
| CI checkout fails: "git failed with exit code 1" | actions/checkout shallow clone can't resolve commit SHA as ref | Send branch name (not SHA) in repository_dispatch. Verify local HEAD matches remote before dispatch. |
| CI signing steps silently skipped | Secret names don't match workflow secrets.XXX references | gh secret list and compare against all secrets. references in workflow YAML |
| "The timestamp service is not available" | Apple's timestamp server intermittently unavailable during codesign | Retry the build. ignore filter reduces files needing timestamps, lowering failure probability. |
| Notarization: "Could not find valid private key" | .p8 file base64 decoded incorrectly | Verify: `echo "$APPLE_API_KEY" \ |
zsh permission denied piping long base64 | Shell interprets base64 special chars as glob | Use temp file + < redirect: gh secret set NAME < /tmp/file.txt |
---
Local Verification (without notarization)
# Sign only (fast verification that certificate works)
FLOWZERO_CODESIGN=1 pnpm run forge:make
# Verify signature
codesign --verify --deep --strict path/to/App.app
# Check signing authority
codesign -dvv path/to/App.app 2>&1 | grep Authority
# Gatekeeper assessment
spctl --assess --type exec path/to/App.appCamera / AVFoundation Reference
Camera Preview Implementation
Complete Working Example
import SwiftUI
import AVFoundation
import os
private let logger = Logger(subsystem: "com.app", category: "Camera")
// MARK: - Session Manager
@MainActor
final class CameraSessionManager: ObservableObject {
@Published private(set) var isRunning = false
@Published private(set) var error: CameraError?
let session = AVCaptureSession()
private var videoInput: AVCaptureDeviceInput?
enum CameraError: LocalizedError {
case noCamera
case setupFailed(String)
case permissionDenied
var errorDescription: String? {
switch self {
case .noCamera: return "No camera available"
case .setupFailed(let reason): return "Setup failed: \(reason)"
case .permissionDenied: return "Camera permission denied"
}
}
}
func start() async {
logger.info("start() called, isRunning=\(self.isRunning)")
guard !isRunning else { return }
// Check permission
guard await requestPermission() else {
error = .permissionDenied
return
}
// Get camera
guard let device = AVCaptureDevice.default(
.builtInWideAngleCamera,
for: .video,
position: .front
) else {
logger.error("No front camera available")
error = .noCamera
return
}
// Configure session
session.beginConfiguration()
session.sessionPreset = .high
do {
let input = try AVCaptureDeviceInput(device: device)
if session.canAddInput(input) {
session.addInput(input)
videoInput = input
}
session.commitConfiguration()
// Start on background thread
await withCheckedContinuation { continuation in
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
self?.session.startRunning()
DispatchQueue.main.async {
self?.isRunning = true
logger.info("Camera session started")
continuation.resume()
}
}
}
} catch {
session.commitConfiguration()
self.error = .setupFailed(error.localizedDescription)
}
}
func stop() {
guard isRunning else { return }
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
self?.session.stopRunning()
DispatchQueue.main.async {
self?.isRunning = false
}
}
}
private func requestPermission() async -> Bool {
switch AVCaptureDevice.authorizationStatus(for: .video) {
case .authorized: return true
case .notDetermined:
return await AVCaptureDevice.requestAccess(for: .video)
default: return false
}
}
}
// MARK: - SwiftUI View
struct CameraPreviewView: UIViewRepresentable {
let session: AVCaptureSession
func makeUIView(context: Context) -> CameraPreviewUIView {
let view = CameraPreviewUIView()
view.backgroundColor = .black
view.session = session
return view
}
func updateUIView(_ uiView: CameraPreviewUIView, context: Context) {}
}
final class CameraPreviewUIView: UIView {
override class var layerClass: AnyClass { AVCaptureVideoPreviewLayer.self }
var previewLayer: AVCaptureVideoPreviewLayer { layer as! AVCaptureVideoPreviewLayer }
var session: AVCaptureSession? {
get { previewLayer.session }
set {
previewLayer.session = newValue
previewLayer.videoGravity = .resizeAspectFill
configureMirroring()
}
}
private func configureMirroring() {
guard let connection = previewLayer.connection,
connection.isVideoMirroringSupported else { return }
// CRITICAL: Must disable automatic adjustment BEFORE setting manual mirroring
// Without this, iOS throws: "Cannot be set when automaticallyAdjustsVideoMirroring is YES"
connection.automaticallyAdjustsVideoMirroring = false
connection.isVideoMirrored = true
}
override func layoutSubviews() {
super.layoutSubviews()
previewLayer.frame = bounds
}
}
// MARK: - Usage in SwiftUI
struct ContentView: View {
@StateObject private var cameraManager = CameraSessionManager()
var body: some View {
ZStack {
// CRITICAL: Use GeometryReader for proper sizing
GeometryReader { geo in
CameraPreviewView(session: cameraManager.session)
.frame(width: geo.size.width, height: geo.size.height)
}
.ignoresSafeArea()
// Overlay content here
}
.onAppear {
Task { await cameraManager.start() }
}
.onDisappear {
cameraManager.stop()
}
}
}Common Issues and Solutions
Issue: Camera preview shows nothing
Debug steps:
1. Check if running on simulator (camera not available):
#if targetEnvironment(simulator)
logger.warning("Camera not available on simulator")
#endif2. Add logging to trace execution:
logger.info("Permission status: \(AVCaptureDevice.authorizationStatus(for: .video).rawValue)")
logger.info("Session running: \(session.isRunning)")
logger.info("Preview layer bounds: \(previewLayer.bounds)")3. Verify Info.plist has camera permission:
<key>NSCameraUsageDescription</key>
<string>Camera access for preview</string>Issue: UIViewRepresentable has zero size
Cause: In ZStack, UIViewRepresentable doesn't expand like SwiftUI views.
Solution: Wrap in GeometryReader with explicit frame:
GeometryReader { geo in
CameraPreviewView(session: session)
.frame(width: geo.size.width, height: geo.size.height)
}Issue: Preview layer connection is nil
Cause: Connection isn't established until session is running and layer is in view hierarchy.
Solution: Configure mirroring in layoutSubviews:
override func layoutSubviews() {
super.layoutSubviews()
previewLayer.frame = bounds
// Retry mirroring here
configureMirroring()
}
private func configureMirroring() {
guard let conn = previewLayer.connection,
conn.isVideoMirroringSupported else { return }
conn.automaticallyAdjustsVideoMirroring = false
conn.isVideoMirrored = true
}Issue: Crash on setVideoMirrored
Error: *** -[AVCaptureConnection setVideoMirrored:] Cannot be set when automaticallyAdjustsVideoMirroring is YES
Cause: iOS automatically adjusts mirroring by default. Setting isVideoMirrored while automatic adjustment is enabled throws an exception.
Solution: Always disable automatic adjustment first:
// WRONG - crashes on some devices
connection.isVideoMirrored = true
// CORRECT - disable automatic first
connection.automaticallyAdjustsVideoMirroring = false
connection.isVideoMirrored = trueAffected Devices: Primarily older devices (iPhone X, etc.) but can affect any device.
Issue: Swift 6 concurrency errors with AVCaptureSession
Error: "cannot access property 'session' with non-Sendable type from nonisolated deinit"
Solution: Don't access session in deinit. Use explicit stop() call:
deinit {
// Don't access session here
// Cleanup handled by stop() call from view
}Debugging with Console.app
1. Open Console.app 2. Select your device 3. Filter by:
- Subsystem:
com.yourapp - Category:
Camera
4. Look for the log sequence:
start() called, isRunning=false
Permission granted
Found front camera: Front Camera
Camera session startedCamera + Audio Conflict
If using AudioKit or AVAudioEngine, camera audio input may conflict.
Solution: Use video-only input, no audio:
// Only add video input, skip audio
let videoInput = try AVCaptureDeviceInput(device: videoDevice)
session.addInput(videoInput)
// Do NOT add audio inputSwiftUI iOS Version Compatibility
iOS 17 vs iOS 16 API Differences
View Modifiers
onChange
// iOS 17+ (dual parameter)
.onChange(of: value) { oldValue, newValue in
// Can compare old and new
}
// iOS 16 (single parameter)
.onChange(of: value) { newValue in
// Only new value available
}sensoryFeedback (iOS 17+)
// iOS 17+
.sensoryFeedback(.impact, trigger: triggerValue)
// iOS 16 fallback
UIImpactFeedbackGenerator(style: .medium).impactOccurred()Views
ContentUnavailableView (iOS 17+)
// iOS 17+
ContentUnavailableView(
"No Results",
systemImage: "magnifyingglass",
description: Text("Try a different search")
)
// iOS 16 fallback
VStack(spacing: 16) {
Image(systemName: "magnifyingglass")
.font(.system(size: 48))
.foregroundStyle(.secondary)
Text("No Results")
.font(.title2.bold())
Text("Try a different search")
.font(.subheadline)
.foregroundStyle(.secondary)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)Inspector (iOS 17+)
// iOS 17+
.inspector(isPresented: $showInspector) {
InspectorContent()
}
// iOS 16 fallback: Use sheet or sidebar
.sheet(isPresented: $showInspector) {
InspectorContent()
}Observation
@Observable Macro (iOS 17+)
// iOS 17+ with @Observable
@Observable
class ViewModel {
var count = 0
}
struct ContentView: View {
var viewModel = ViewModel()
var body: some View {
Text("\(viewModel.count)")
}
}
// iOS 16 with ObservableObject
class ViewModel: ObservableObject {
@Published var count = 0
}
struct ContentView: View {
@StateObject var viewModel = ViewModel()
var body: some View {
Text("\(viewModel.count)")
}
}Audio
AVAudioApplication (iOS 17+)
// iOS 17+
let permission = AVAudioApplication.shared.recordPermission
AVAudioApplication.requestRecordPermission { granted in }
// iOS 16
let permission = AVAudioSession.sharedInstance().recordPermission
AVAudioSession.sharedInstance().requestRecordPermission { granted in }Animations
Symbol Effects (iOS 17+)
// iOS 17+
Image(systemName: "heart.fill")
.symbolEffect(.bounce, value: isFavorite)
// iOS 16 fallback
Image(systemName: "heart.fill")
.scaleEffect(isFavorite ? 1.2 : 1.0)
.animation(.spring(), value: isFavorite)Data
SwiftData (iOS 17+)
// iOS 17+ with SwiftData
@Model
class Item {
var name: String
var timestamp: Date
}
// iOS 16: Use CoreData or third-party (Realm)
// CoreData: NSManagedObject subclass
// Realm: Object subclass with @Persisted propertiesConditional Compilation
For features that must use iOS 17 APIs when available:
if #available(iOS 17.0, *) {
ContentUnavailableView("Title", systemImage: "icon")
} else {
LegacyEmptyView()
}For view modifiers:
extension View {
@ViewBuilder
func onChangeCompat<V: Equatable>(of value: V, perform: @escaping (V) -> Void) -> some View {
if #available(iOS 17.0, *) {
self.onChange(of: value) { _, newValue in
perform(newValue)
}
} else {
self.onChange(of: value, perform: perform)
}
}
}Minimum Deployment Targets by Feature
| Feature | Minimum iOS |
|---|---|
| SwiftUI basics | 13.0 |
| @StateObject | 14.0 |
| AsyncImage | 15.0 |
| .searchable | 15.0 |
| NavigationStack | 16.0 |
| .navigationDestination | 16.0 |
| @Observable | 17.0 |
| ContentUnavailableView | 17.0 |
| SwiftData | 17.0 |
| .onChange (dual param) | 17.0 |
Testing @MainActor Classes
The Problem
Testing @MainActor classes like ObservableObject controllers in Swift 6 is challenging because:
1. setUp() and tearDown() are nonisolated 2. Properties with private(set) can't be set from tests 3. Direct property access from tests triggers concurrency errors
Solution: setStateForTesting Pattern
Add a DEBUG-only method to allow tests to set internal state:
@MainActor
final class TrainingSessionController: ObservableObject {
@Published private(set) var state: TrainingState = .idle
// ... rest of controller ...
// MARK: - Testing Support
#if DEBUG
/// Set state directly for testing purposes only
func setStateForTesting(_ newState: TrainingState) {
state = newState
}
#endif
}Test Class Structure
import XCTest
@testable import YourApp
@MainActor
final class TrainingSessionControllerTests: XCTestCase {
var controller: TrainingSessionController!
override func setUp() {
super.setUp()
controller = TrainingSessionController()
}
override func tearDown() {
controller = nil
super.tearDown()
}
func testConfigureFromFailedStateAutoResets() {
// Arrange: Set to failed state
controller.setStateForTesting(.failed("Recording too short"))
// Act: Configure should recover
controller.configure(with: PhaseConfig.default)
// Assert: Should be back in idle
XCTAssertEqual(controller.state, .idle)
}
}State Machine Testing Patterns
Testing State Transitions
func testStateTransitions() {
// Test each state's behavior
let states: [TrainingState] = [.idle, .completed, .failed("error")]
for state in states {
controller.setStateForTesting(state)
controller.configure(with: PhaseConfig.default)
// Verify expected outcome
XCTAssertTrue(controller.canStart, "\(state) should allow starting")
}
}Regression Tests
For bugs that have been fixed, add specific regression tests:
/// Regression test for: State machine dead-lock after recording failure
/// Bug: After error, controller stayed in failed state forever
func testRegressionFailedStateDeadLock() {
// Simulate the bug scenario
controller.configure(with: PhaseConfig.default)
controller.setStateForTesting(.failed("录音太短"))
// The fix: configure() should auto-reset from failed state
controller.configure(with: PhaseConfig.default)
XCTAssertEqual(controller.state, .idle,
"REGRESSION: Failed state should not block configure()")
}State Machine Invariants
Test invariants that should always hold:
/// No terminal state should become a "dead end"
func testAllTerminalStatesAreRecoverable() {
let terminalStates: [TrainingState] = [
.idle,
.completed,
.failed("test error")
]
for state in terminalStates {
controller.setStateForTesting(state)
// Action that should recover
controller.configure(with: PhaseConfig.default)
// Verify recovery
XCTAssertTrue(canConfigure(),
"\(state) should be recoverable via configure()")
}
}Why This Pattern Works
1. `#if DEBUG`: Method only exists in test builds, zero production overhead 2. Explicit method: Makes test-only state manipulation obvious and searchable 3. MainActor compatible: Method is part of the @MainActor class 4. Swift 6 safe: Avoids concurrency errors by staying on the main actor
Alternative: Internal Setter
If you prefer, you can use internal(set) instead of private(set):
@Published internal(set) var state: TrainingState = .idleHowever, this is redundant since properties are already internal by default. The setStateForTesting() pattern is more explicit about test-only intent.
XcodeGen Complete Reference
Full project.yml Structure
name: ProjectName
options:
bundleIdPrefix: com.company
deploymentTarget:
iOS: "16.0"
macOS: "13.0"
xcodeVersion: "16.0"
generateEmptyDirectories: true
createIntermediateGroups: true
settings:
base:
SWIFT_VERSION: "6.0"
MARKETING_VERSION: "1.0.0"
CURRENT_PROJECT_VERSION: "1"
packages:
# Basic package
PackageName:
url: https://github.com/org/repo
from: "1.0.0"
# Exact version
ExactPackage:
url: https://github.com/org/repo
exactVersion: "2.0.0"
# Branch
BranchPackage:
url: https://github.com/org/repo
branch: main
# Local package
LocalPackage:
path: ../LocalPackage
targets:
MainApp:
type: application
platform: iOS
sources:
- path: Sources
excludes:
- "**/.DS_Store"
- "**/Tests/**"
- path: Resources
type: folder
settings:
base:
INFOPLIST_FILE: Sources/Info.plist
PRODUCT_BUNDLE_IDENTIFIER: com.company.app
ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME: AccentColor
LD_RUNPATH_SEARCH_PATHS: "$(inherited) @executable_path/Frameworks"
ENABLE_BITCODE: NO
CODE_SIGN_STYLE: Automatic
DEVELOPMENT_TEAM: TEAM_ID
configs:
Debug:
SWIFT_OPTIMIZATION_LEVEL: -Onone
Release:
SWIFT_OPTIMIZATION_LEVEL: -O
dependencies:
# SPM package
- package: PackageName
# SPM package with explicit product
- package: Firebase
product: FirebaseAnalytics
# Another target
- target: Framework
# System framework
- framework: UIKit.framework
# SDK
- sdk: CoreLocation.framework
preBuildScripts:
- name: "Run Script"
script: |
echo "Pre-build script"
runOnlyWhenInstalling: false
postBuildScripts:
- name: "Post Build"
script: |
echo "Post-build script"
Tests:
type: bundle.unit-test
platform: iOS
sources:
- path: Tests
dependencies:
- target: MainApp
settings:
base:
TEST_HOST: "$(BUILT_PRODUCTS_DIR)/MainApp.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/MainApp"
BUNDLE_LOADER: "$(TEST_HOST)"Target Types
| Type | Description |
|---|---|
application | iOS/macOS app |
framework | Dynamic framework |
staticFramework | Static framework |
bundle.unit-test | Unit test bundle |
bundle.ui-testing | UI test bundle |
app-extension | App extension |
watch2-app | watchOS app |
widget-extension | Widget extension |
Build Settings Reference
Common Settings
settings:
base:
# Versioning
MARKETING_VERSION: "1.0.0"
CURRENT_PROJECT_VERSION: "1"
# Swift
SWIFT_VERSION: "6.0"
SWIFT_STRICT_CONCURRENCY: complete
# Signing
CODE_SIGN_STYLE: Automatic
DEVELOPMENT_TEAM: TEAM_ID
CODE_SIGN_IDENTITY: "Apple Development"
# Deployment
IPHONEOS_DEPLOYMENT_TARGET: "16.0"
TARGETED_DEVICE_FAMILY: "1,2" # 1=iPhone, 2=iPad
# Build
ENABLE_BITCODE: NO
DEBUG_INFORMATION_FORMAT: dwarf-with-dsym
# Paths
LD_RUNPATH_SEARCH_PATHS: "$(inherited) @executable_path/Frameworks"Per-Configuration Settings
settings:
configs:
Debug:
SWIFT_OPTIMIZATION_LEVEL: -Onone
SWIFT_ACTIVE_COMPILATION_CONDITIONS: DEBUG
MTL_ENABLE_DEBUG_INFO: INCLUDE_SOURCE
Release:
SWIFT_OPTIMIZATION_LEVEL: -O
SWIFT_COMPILATION_MODE: wholemodule
VALIDATE_PRODUCT: YESInfo.plist Keys
Common keys to add:
<key>NSCameraUsageDescription</key>
<string>Camera access description</string>
<key>NSMicrophoneUsageDescription</key>
<string>Microphone access description</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Photo library access description</string>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
<string>location</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
</array>Related skills
FAQ
What Apple build tools does developing-ios-apps cover?
developing-ios-apps covers XcodeGen for project.yml, SwiftUI for UI, Swift Package Manager for dependencies, notarytool for macOS notarization, and GitHub Actions for CI/CD pipelines including certificate and provisioning setup.
Which signing errors does developing-ios-apps address?
developing-ios-apps addresses Error -25294, keychain mismatch, adhoc fallback, EMFILE, notarization credential conflicts, and Library not loaded @rpath failures common during real-device deployment and release builds.
Is Developing Ios Apps safe to install?
skills.sh reports 2 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.