Install
$ agentstack add skill-markdavidgan-apple-dev-skills-ios-build Open-source listing — not yet scanned by AgentStack. Follow the source repository for install instructions.
Security review
⚠ Flagged1 finding(s); flagged for manual review. · v0.1.0 How review works →
- • Prompt-injection patterns
- • Secret / credential exfiltration
- • Dangerous shell & filesystem operations
- • Untrusted network calls
- • Known-malicious package signatures
- high Destructive filesystem operation.
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
iOS Build
Build system patterns, validation workflows, and troubleshooting. Run validation before every commit.
The 4-Layer Validation Pipeline
Use the lightest layer that matches your situation.
| Layer | Command | Time | What It Catches | |-------|---------|------|-----------------| | 1. Fast | make validate-fast | ~30s | Lint, isolation violations, safety patterns | | 2. Full | make validate | ~3 min | Layer 1 + archive builds (Swift 6 strict concurrency) | | 3. Export Test | make export-test-{app} | ~5 min | Layer 2 + signing, provisioning, icons, entitlements | | 4. Upload | bundle exec fastlane alpha | ~10 min | Layer 3 + Apple Transporter, TestFlight processing |
When to Use Each Layer
- During development:
make validate-fastafter each significant change - Before committing:
make validateat minimum - Before pushing:
make export-test-{app}for the affected app(s) - Release:
bundle exec fastlane alphato upload
Critical Rule: Archive vs Debug Builds
Debug/simulator builds do NOT catch all strict concurrency errors.
Swift 6 strict concurrency checking is more thorough in optimized builds:
# Debug build — misses some concurrency errors
xcodebuild -scheme MyApp -destination 'platform=iOS Simulator,name=iPhone 17 Pro Max' build
# Archive build — catches ALL concurrency errors
xcodebuild -scheme MyApp -destination 'generic/platform=iOS' archive
Always run archive builds before committing Swift code changes.
XcodeGen
Project Structure
# project.yml
name: MyApp
targets:
MyApp-iOS:
type: application
platform: iOS
deploymentTarget: "26.0"
sources:
- MyApp-iOS
dependencies:
- target: MyAppKit
- sdk: HealthKit.framework
- sdk: Speech.framework
settings:
base:
SWIFT_STRICT_CONCURRENCY: complete
SWIFT_DEFAULT_ACTOR_ISOLATION: MainActor
PRODUCT_BUNDLE_IDENTIFIER: com.example.myapp
info:
path: MyApp-iOS/Info.plist
properties:
UISupportedInterfaceOrientations: [UIInterfaceOrientationPortrait]
MyAppKit:
type: framework
platform: iOS
sources:
- MyAppKit/Sources
Regenerating Project
# After any project.yml change
xcodegen generate
# With specific spec
xcodegen generate --spec project.yml
Adding Files
Do NOT manually edit .xcodeproj. XcodeGen automatically includes files from configured source directories.
sources:
- MyApp-iOS # All .swift files included
- path: Resources # Non-code resources
buildPhase: resources
XcodeGen Hyphen to Underscore
Critical: XcodeGen converts hyphens in target names to underscores in Swift module names:
# project.yml
targets:
MyApp-iOS: # Target name with hyphen
# ...
// Generated Swift module uses underscore
@testable import MyApp_iOS // Not MyApp-iOS
Watch App Configuration
Critical: Watch apps require specific XcodeGen configuration to embed properly:
# WRONG — causes Xcode Cloud archive failures
targets:
MyApp-iOS:
scheme:
buildTargets: # Don't include Watch targets here!
- MyApp-iOS
- MyApp-Watch # ❌ Breaks Xcode Cloud
# RIGHT — embed via target dependency only
targets:
MyApp-iOS:
dependencies:
- target: MyApp-Watch
embed: true
copyFiles:
- destination: products/Watch
subpath: MyAppWatch.app
MyApp-Watch:
type: application.watchapp2
platform: watchOS
deploymentTarget: "10.0"
sources:
- MyApp-Watch
Key rules:
- Never add Watch targets to the iOS scheme's
buildTargets— breaks Xcode Cloud - Use
embed: truewithcopyFilestoproducts/Watch - Watch app will build automatically via target dependency
Watch App Icon — Xcode 17 iphoneos Thinning (Gotcha)
When archiving with -destination generic/platform=iOS, Xcode 17 runs actool on the embedded Watch bundle with --platform iphoneos as part of the iOS archive's thinning pass. If your Watch AppIcon.appiconset/Contents.json only contains a "platform": "watchos" entry, actool throws:
error: The app icon set named "AppIcon" did not have any applicable content.
Fix: Add a second "idiom": "universal" entry (no platform field) pointing to the same 1024×1024 file. The watchOS-specific entry continues to handle proper CFBundleIconName assignment for the Watch bundle; the universal entry gives iphoneos thinning something to find.
{
"images": [
{
"filename": "AppIcon.png",
"idiom": "universal",
"platform": "watchos",
"size": "1024x1024"
},
{
"filename": "AppIcon.png",
"idiom": "universal",
"size": "1024x1024"
}
],
"info": { "author": "xcode", "version": 1 }
}
The "unassigned child" warning emitted for the universal entry is harmless — it's a warning, not an error, and does not affect the archive result or altool validation.
Never Pass -sdk iphoneos to xcodebuild (Xcode 17)
Passing -sdk iphoneos to an xcodebuild archive command forces all targets — including the embedded Watch app — to compile against the iOS SDK. In Xcode 17 this causes the Watch build to fail outright.
# ❌ Broken in Xcode 17 — Watch targets compile against iOS SDK
xcodebuild -scheme MyApp-iOS -sdk iphoneos archive ...
# ✅ Correct — each platform target uses its own SDK automatically
xcodebuild -scheme MyApp-iOS -destination generic/platform=iOS archive ...
Remove -sdk iphoneos from any Fastfile gym/xcodebuild invocation and use -destination generic/platform=iOS only.
Gating a Privacy-Sensitive Symbol Per-App in a Shared SPM Package
Problem: A shared SPM package (e.g. a monorepo's Kit) is linked by several apps. One app needs a privacy-sensitive symbol (AVCaptureDevice, CLLocationManager, CMMotionManager, HKHealthStore); the others don't. If the symbol lives in the shared package, every app links it, and Apple's binary scanner then demands the corresponding Info.plist usage string from apps that have no such feature — which human review rejects as a false feature (Guideline 5.1.1). See apple-review → "Privacy Symbols vs Usage Strings."
Why SPM traits don't solve this in Xcode: SwiftPM package traits (the traits:/enabledTraits: feature) look like the answer, but they cannot be toggled per-target from an Xcode .xcodeproj. There's no app-level Package.swift to enable a dependency's trait; the .xcodeproj just references the product. (Traits work when the consumer is itself a SwiftPM package.) Don't reach for them in an XcodeGen/.xcodeproj app.
Reliable Xcode-native fix — separate product + dependency inversion:
- In the shared package, split the symbol into its own product/target. Core stays symbol-free; it declares a protocol + a set-once registry instead of calling the symbol directly:
``swift // Core target — no AVCaptureDevice anywhere public protocol CameraPermissionProviding: Sendable { /* status/request */ } public enum CameraPermissionRegistry { private static let storage = Mutex(nil) // Synchronization public static func register(_ p: any CameraPermissionProviding) { storage.withLock { $0 = p } } public static var provider: (any CameraPermissionProviding)? { storage.withLock { $0 } } } ` `swift // Separate "Camera" target/product — the ONLY AVCaptureDevice site public struct AVCaptureCameraProvider: CameraPermissionProviding { /* calls AVCaptureDevice */ } public enum AppCamera { public static func install() { CameraPermissionRegistry.register(AVCaptureCameraProvider()) } } ` `swift // Package.swift .library(name: "KitCamera", targets: ["KitCamera"]), .target(name: "KitCamera", dependencies: ["Kit"], path: "Sources/KitCamera"), ``
- Only camera-using apps add the product in
project.ymland callinstall()once at launch:
```yaml dependencies:
- package: Kit
product: Kit # XcodeGen defaults - package: Kit to the same-named product;
- package: Kit # list products explicitly when a package has more than one.
product: KitCamera `` `swift // App launch (camera-using app only) AppCamera.install() ` Apps that don't link KitCamera` get the core's safe no-op fallback and never link the symbol.
Verify at the binary level (don't trust source-grep alone):
make archive- # build a FRESH archive — stale ones in build/ predate the fix
APP=.../.xcarchive/Products/Applications/.app
nm "$APP/" | grep -i AVCaptureDevice # opted-out app → zero matches
grep -rl AVCaptureDevice "$APP" # full-bundle scan incl. embedded frameworks
otool -L "$APP/" | grep -i AVFoundation # positive control on the opted-in app
Run the same nm on a camera-using app as a positive control — it should show the symbol. App-level Swift compile flags do not propagate into a local SwiftPM dependency, so you cannot gate the symbol with an app-target #if; the product boundary is what does the gating.
Build Commands
Simulator Build (Fast)
xcodebuild -scheme MyApp-iOS \
-destination 'platform=iOS Simulator,name=iPhone 17 Pro Max' \
build 2>&1 | grep -E "error:|Build succeeded"
Device Build
xcodebuild -scheme MyApp-iOS \
-destination 'generic/platform=iOS' \
build
Archive Build (Required for Validation)
xcodebuild -scheme MyApp-iOS \
-destination 'generic/platform=iOS' \
-archivePath build/MyApp.xcarchive \
archive
Export Test (CI-Equivalent)
# Create export options plist
cat > exportOptions.plist
method
app-store
teamID
YOUR_TEAM_ID
EOF
# Export archive
xcodebuild -exportArchive \
-archivePath build/MyApp.xcarchive \
-exportOptionsPlist exportOptions.plist \
-exportPath build/ipa
Testing
Unit Tests (Package)
cd MyAppKit && swift test
Unit Tests (Xcode)
xcodebuild test -scheme MyApp-iOS \
-destination 'platform=iOS Simulator,name=iPhone 17 Pro Max' \
-only-testing:MyAppTests
UI Tests
xcodebuild test -scheme MyApp-iOS-UITests \
-destination 'platform=iOS Simulator,name=iPhone 17 Pro Max'
Specific Test
xcodebuild test -scheme MyApp-iOS \
-destination 'platform=iOS Simulator,name=iPhone 17 Pro Max' \
-only-testing:MyAppTests/TimerViewModelTests/test_startFromIdle
Common Build Errors
Strict Concurrency Errors
Error: Call to main actor-isolated instance method in a synchronous nonisolated context
Fix: Add @MainActor annotation:
// Before
class ViewModel {
func update() { }
}
// After
@MainActor
class ViewModel {
func update() { }
}
Sendable Errors
Error: Non-sendable type 'X' returned by call crossing isolation boundary
Fix: Use @preconcurrency import or extract values:
@preconcurrency import EventKit
@MainActor
final class Service {
private let store = EKEventStore()
}
Task Isolation Errors
Error: Reference to captured var 'self' in concurrently-executing code
Fix: Use [weak self] with explicit isolation:
// Wrong
Task {
await self.update()
}
// Right
Task { @MainActor [weak self] in
guard let self = self else { return }
await self.update()
}
Concurrent Archives Corrupt Shared DerivedData
Error (either signature — same root cause):
error: unable to write file '.../DerivedData/.../--VFS-iphoneos/all-product-headers.yaml':
No such file or directory (2)
** ARCHIVE FAILED **
error: Unable to resolve module dependency: 'CoreFoundation' (in target 'MyKit' from project 'MyKit')
error: Unable to resolve module dependency: 'Darwin' …
error: Unable to resolve module dependency: 'UIKit' …
** ARCHIVE FAILED **
The second signature is the more misleading one: it names system modules (CoreFoundation/Darwin/UIKit/Dispatch/QuartzCore) against a shared Kit/SPM target, so it reads like "your code references something that doesn't exist." It doesn't — the build just couldn't read the cached system module maps because a parallel build was rewriting/locking them. Do not edit code or rm -rf the cache reflexively. Confirm with pgrep -f "xcodebuild|swift-frontend" first.
Cause: Two xcodebuild runs (archive or build) sharing one DerivedData path race on the same VFS overlay/header-map intermediates and clobber each other. This is not a code, Gemfile, or signing failure — it is pure build-directory contention. It bites whenever archives run in parallel: two terminals, two CI jobs on one runner, or two agents in a shared checkout. A custom shared build location (Xcode → Locations → Derived Data → Custom) makes every project collide, not just two of the same app.
Fix (durable): give each archive its own DerivedData so they can never share intermediates:
xcodebuild -scheme MyApp-Archive -configuration Release archive \
-archivePath build/MyApp.xcarchive \
-derivedDataPath build/DerivedData \ # per-app/per-job, isolated
-destination generic/platform=iOS
Fix (operational): if you can't isolate paths, serialize — never start an archive while another is in flight. Gate on pgrep -f "xcodebuild|swift-frontend" before launching.
Recovery: a poisoned archive leaves corrupt intermediates that fail repeat runs. With the isolated path above, rm -rf build/DerivedData and retry. On a shared path, only clear the corrupt …/ArchiveIntermediates/ dir while nothing else is building, or you break the other job.
Missing Provisioning Profile
Error: No profiles for 'com.example.app' were found
Fix: Use ASC MCP tools to check and fix signing:
# Check signing status against Developer Portal
asc_check_signing --bundle-id com.example.app --capabilities push-notifications,app-groups
# List available certificates and profiles
asc_list_certificates
asc_list_profiles --type IOS_APP_DEVELOPMENT
# Create a new certificate if needed
asc_create_certificate --type DISTRIBUTION
DerivedData Issues
# Clear DerivedData for this project
rm -rf ~/Library/Developer/Xcode/DerivedData/MyApp-*
# Clear all DerivedData (nuclear option)
rm -rf ~/Library/Developer/Xcode/DerivedData
SwiftData Model Changes
Error: Crash after changing @Model properties
Fix: Delete app to recreate database:
xcrun simctl uninstall booted com.example.myapp
CI/CD Integration
GitHub Actions Example
name: Build and Test
on: [push, pull_request]
jobs:
test:
runs-on: macos-15
steps:
- uses: actions/checkout@v4
- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.4.8
bundler-cache: true
- name: Install XcodeGen
run: brew install xcodegen
- name: Generate Project
run: xcodegen generate
- name: Run Package Tests
run: cd MyAppKit && swift test
- name: Archive Build (catches concurrency errors)
run: |
xcodebuild archive \
-scheme MyApp-iOS \
-destination 'generic/platform=iOS' \
-archivePath build/MyApp.xcarchive
- name: Run Unit Tests
run: |
xcodebuild test \
-scheme MyApp-iOS \
-destination 'platform=iOS Simulator,name=iPhone 17 Pro Max' \
-only-testing:MyAppTests | xcpretty
Fastlane Integration
# fastlane/Fastfile
lane :test do
run_tests(scheme: "MyApp-iOS")
end
lane :archive do
bui
…
## Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- **Author:** [markdavidgan](https://github.com/markdavidgan)
- **Source:** [markdavidgan/apple-dev-skills](https://github.com/markdavidgan/apple-dev-skills)
- **License:** MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.