swift

star 0

Swift project layout + xcodebuild gates for native iOS/macOS apps

leonardoacosta By leonardoacosta schedule Updated 6/8/2026

name: swift description: Swift project layout + xcodebuild gates for native iOS/macOS apps category: Framework level: library engineer: ui-engineer gate: "xcodebuild build -scheme App" bundles: [] audit-rubric: # TODO: fill in allowed-tools: Read, Glob, Grep, Bash

Swift

Native iOS/macOS/watchOS development for XcodeGen-based projects (the nexus / nx fleet: a menubar Mac app + iOS + watch + shared framework). Covers project conventions, headless compile verification from Linux, signing, and the device-deploy gotchas.

XcodeGen projects (project.yml is the source of truth)

nexus.xcodeproj is generated — do not hand-edit project.pbxproj. Targets + sources live in apps/swift/project.yml:

nexus-ios:
  type: application
  platform: iOS
  sources:
    - path: nexus-ios/Sources   # DIRECTORY GLOB — any .swift under here auto-includes
  • Adding a file = drop it under the globbed Sources/ dir + xcodegen generate. No PBXFileReference/PBXBuildFile/build-phase surgery. Verify: grep -c MyFile.swift nexus.xcodeproj/project.pbxproj (expect 4 — fileRef, buildFile, group, sources phase).
  • The .xcodeproj IS tracked (committed-generated). After xcodegen generate adds a file, commit the regenerated pbxproj so teammates who open Xcode without running xcodegen get it.
  • project.pbxproj and */Generated/Info.plist are XcodeGen output — a dirty diff on them is regen drift, not work. Don't stash/commit-as-precious; xcodegen generate overwrites them.
  • Targets in nx: nexus-ios (iphoneos), nexus-mac (menubar), nexus-watch, NexusShared (shared framework), MarkdownUI. HealthKit/biometrics is iOS-onlynexus-ios.

Headless compile verification (Linux → Mac over SSH)

You cannot run xcodebuild on Linux, but you can verify a self-contained file (imports only Foundation + a system framework, no app-type deps) against the real SDK over SSH:

scp File.swift mac:/tmp/
ssh mac 'xcrun --sdk iphoneos swiftc -typecheck -target arm64-apple-ios16.0 /tmp/File.swift; echo EXIT=$?'
# EXIT=0 + zero diagnostics = clean. Pass multiple files together to check integration.
ssh mac 'plutil -lint /tmp/Info.plist /tmp/App.entitlements'   # validate plists/entitlements

Keep new modules self-contained (e.g. an actor that does its own URLSession POST + endpoint resolution via Bundle.main.object(forInfoDictionaryKey:)) so they type-check in isolation and drop in cleanly.

Signing + headless capability registration (no Developer-portal clicking)

  • nx uses Automatic signing (CODE_SIGN_STYLE: Automatic, DEVELOPMENT_TEAM: DX3Y367L2A).
  • An App Store Connect API key (~/.appstoreconnect/private_keys/AuthKey_<KEYID>.p8) lets tooling hit Apple's portal with NO interactive Apple-ID/2FA login:
    • xcodebuild ... -allowProvisioningUpdates -authenticationKeyID <id> -authenticationKeyIssuerID <iss> -authenticationKeyPath <p8> auto-registers new capabilities (e.g. HealthKit) on the App ID + regenerates the profile. Adding the entitlement to *.entitlements + this flag is usually enough — no manual portal step.
    • fastlane lanes (sigh_ios, ensure_bundle_id) drive Spaceship::ConnectAPI with the same .p8 (fastlane produce does NOT support API-key auth; Spaceship does).
  • Add a capability = add its key to <target>/Resources/<target>.entitlements (e.g. com.apple.developer.healthkit + …healthkit.background-delivery) + a usage string in Info.plist (e.g. NSHealthShareUsageDescription). XcodeGen carries the entitlements path.

THE codesign gotcha: signing needs a GUI (Aqua) session — SOLVED for iOS

codesign with the team identity (8E12…/DX3Y367L2A) fails with errSecInternalComponent inside a background SSH session (managername=Background). Root cause (nx-tceo6): NOT a locked keychain — the login keychain is already unlocked in the permanently-logged-in console session. It is a session-CONTEXT block: the team identity only signs inside the Aqua (GUI) session.

Both macOS and iOS work around this with the gui/501 Aqua bridge: when NOT in Aqua, the SSH-side script launchctl kickstarts a GUI-scoped LaunchAgent that re-enters the signed build inside the Aqua session (where signing works), writes an OK/SKIP/FAIL marker, and the SSH side polls that marker.

  • macOS Nexus.app: deploy/lib/macos-swift-deploy.sh + dev.leonardoacosta.nexus.deploy.
  • iOS device install (SOLVED): deploy/lib/ios-device-deploy.sh + deploy/launchagents/dev.leonardoacosta.nexus.ios-deploy.plist. This is no longer a hand-off to Leo — a signed iOS device install runs fully headless over ssh mac.

Headless iOS device install over SSH (the working procedure)

# 0. Discover the device UDID (device shows available(paired) when reachable):
ssh mac 'xcrun devicectl list devices'
# 1. One-time: load the GUI LaunchAgent into gui/501 (no sudo — see uid-501 note):
ssh mac '~/dev/nx/deploy/ios-deploy.sh --install'   # also run by deploy/install.sh
# 2. Build (signed, in Aqua) + install on the device, headless:
ssh mac '~/dev/nx/deploy/ios-deploy.sh --device <UDID>'
#   -> non-Aqua caller kickstarts gui/501/<label>, polls the marker, returns 0 on OK.
#      Success prints devicectl's "App installed:" block + bundleID + installationURL.

The bridge internals (clone these conventions for any new GUI-session-bound task): the lib detects launchctl managername != Aqua, writes the target UDID to a sentinel file (~/Library/Application Support/Nexus/ios-deploy-device.txt), resets the marker, then launchctl kickstart -k gui/501/dev.leonardoacosta.nexus.ios-deploy. The agent wrapper (deploy/lib/ios-deploy-agent.sh) runs INSIDE Aqua: xcodegen generate; signed xcodebuild build -scheme nexus-ios -destination 'generic/platform=iOS' -allowProvisioningUpdates DEVELOPMENT_TEAM=DX3Y367L2A CODE_SIGN_STYLE=Automatic; locates the .app under Build/Products/Debug-iphoneos/; xcrun devicectl device install app; best-effort device process launch; writes the marker. Log: ~/Library/Logs/nexus-ios-deploy.log.

uid-501 / no-sudo note: over ssh mac you are uid 501, the SAME uid as the console/Aqua user → you can launchctl bootstrap/kickstart gui/501/<label> WITHOUT sudo. There is NO passwordless sudo, so launchctl asuser is NOT an option — the gui/501 kickstart IS the bridge.

Device must be UNLOCKED to LAUNCH (not to install). devicectl device install succeeds on a locked phone; device process launch returns FBSOpenApplicationErrorDomain error 7 (Locked) until the screen is unlocked. The bridge treats install as the contract and launch as best-effort.

Device install (modern toolchain, raw)

xcrun devicectl list devices                                   # paired devices over coredevice net (no cable)
xcrun devicectl device install app --device <UDID> <Built.app> # install (works on a locked device)
xcrun devicectl device process launch --device <UDID> <bundle.id>  # requires the device be unlocked

ios-deploy is legacy and not installed on this Mac — use devicectl. A device shows available (paired) when reachable; unavailable when off/asleep. The raw commands only sign in the Aqua session — over SSH, drive them through deploy/ios-deploy.sh (the gui/501 bridge above).

nx quick reference

  • Mac over SSH: ssh mac (config alias; user leonardoacosta, key ~/.ssh/id_ed25519), Xcode 26.4, repo at /Users/leonardoacosta/dev/nx. (macbook-pro/wrong-user fails.)
  • Networking: Network.postJSON(url:body:) helper; endpoints resolve from an Info.plist key with a http://homelab:<port> fallback (see ApnsRegistrar — the canonical actor+endpoint pattern to clone for any homelab-push module).
  • Prefs: NexusShared.SettingsStore (UserDefaults-backed, typed surface, shared across targets).
  • iOS app entry: nexus-ios/Sources/App/NexusIOSApp.swift (@UIApplicationDelegateAdaptor NexusAppDelegate — bootstrap background work from didFinishLaunchingWithOptions).
  • Naming watch-out: nx already uses "health" for SYSTEM metrics (CPU/mem/disk — HealthSummaryScene/HealthCollector). Apple biometric HealthKit work must be named distinctly (e.g. HealthKit*) to avoid the collision.
Install via CLI
npx skills add https://github.com/leonardoacosta/central-claude --skill swift
Repository Details
star Stars 0
call_split Forks 0
navigation Branch main
article Path SKILL.md
More from Creator
leonardoacosta
leonardoacosta Explore all skills →