AgentStack
SKILL unreviewed MIT Self-run

Ios Build

skill-markdavidgan-apple-dev-skills-ios-build · by markdavidgan

iOS build system patterns — the 4-layer validation pipeline (fast/full/export/upload), XcodeGen project config, archive-vs-debug concurrency checks, and common build-failure fixes. Use for build errors, validation before commit, signing/export problems, XcodeGen setup, or CI/CD configuration. Trigger on "build failing", "validate", "xcodebuild error", "XcodeGen", "archive build", or "set up CI".

No reviews yet
0 installs
3 views
0.0% view→install

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

⚠ Flagged

1 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.

Are you the author of Ios Build? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

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-fast after each significant change
  • Before committing: make validate at minimum
  • Before pushing: make export-test-{app} for the affected app(s)
  • Release: bundle exec fastlane alpha to 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:

  1. Never add Watch targets to the iOS scheme's buildTargets — breaks Xcode Cloud
  2. Use embed: true with copyFiles to products/Watch
  3. 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:

  1. 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"), ``

  1. Only camera-using apps add the product in project.yml and call install() 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.

Versions

  • v0.1.0 Imported from the upstream source.