### Added - **New Skill**: iOS-APP-developer v1.1.0 - iOS development with XcodeGen, SwiftUI, and SPM - XcodeGen project.yml configuration - SPM dependency resolution - Device deployment and code signing - Camera/AVFoundation debugging - iOS version compatibility handling - Library not loaded @rpath framework error fixes - State machine testing patterns for @MainActor classes - Bundled references: xcodegen-full.md, camera-avfoundation.md, swiftui-compatibility.md, testing-mainactor.md - **New Skill**: promptfoo-evaluation v1.0.0 - LLM evaluation framework using Promptfoo - Promptfoo configuration (promptfooconfig.yaml) - Python custom assertions - llm-rubric for LLM-as-judge evaluations - Few-shot example management - Model comparison and prompt testing - Bundled reference: promptfoo_api.md ### Changed - Updated marketplace version from 1.16.0 to 1.18.0 - Updated marketplace skills count from 23 to 25 - Updated skill-creator to v1.2.2: - Fixed best practices documentation URL (platform.claude.com) - Enhanced quick_validate.py to exclude file:// prefixed paths from validation - Updated marketplace.json metadata description to include new skills 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
306 lines
9.2 KiB
Markdown
306 lines
9.2 KiB
Markdown
---
|
|
name: developing-ios-apps
|
|
description: Develops iOS applications with XcodeGen, SwiftUI, and SPM. Triggers on XcodeGen project.yml configuration, SPM dependency issues, device deployment problems, code signing errors, camera/AVFoundation debugging, iOS version compatibility, or "Library not loaded @rpath" framework errors. Use when building iOS apps, fixing Xcode build failures, or deploying to real devices.
|
|
---
|
|
|
|
# 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](#spm-dynamic-framework-not-embedded) |
|
|
| `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](#camera--avfoundation) |
|
|
|
|
## 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
|
|
|
|
```yaml
|
|
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.
|
|
|
|
```yaml
|
|
# In target settings
|
|
settings:
|
|
base:
|
|
CODE_SIGN_STYLE: Automatic
|
|
DEVELOPMENT_TEAM: TEAM_ID # Get from Xcode → Settings → Accounts
|
|
```
|
|
|
|
**Get Team ID**:
|
|
```bash
|
|
security find-identity -v -p codesigning | head -3
|
|
```
|
|
|
|
## iOS 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`:
|
|
```yaml
|
|
deploymentTarget:
|
|
iOS: "16.0"
|
|
```
|
|
|
|
2. Fix incompatible APIs:
|
|
```swift
|
|
// 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
|
|
```
|
|
|
|
3. 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)
|
|
|
|
```bash
|
|
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: `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 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
|
|
- `embed: true` in 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**:
|
|
```bash
|
|
# 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
|
|
|
|
```yaml
|
|
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)
|
|
|
|
```bash
|
|
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
|
|
|
|
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:
|
|
|
|
```swift
|
|
// 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:
|
|
|
|
```swift
|
|
// 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:
|
|
|
|
```swift
|
|
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](references/camera-avfoundation.md)
|
|
|
|
## Resources
|
|
|
|
- [references/xcodegen-full.md](references/xcodegen-full.md) - Complete project.yml options
|
|
- [references/swiftui-compatibility.md](references/swiftui-compatibility.md) - iOS version API differences
|
|
- [references/camera-avfoundation.md](references/camera-avfoundation.md) - Camera preview debugging
|
|
- [references/testing-mainactor.md](references/testing-mainactor.md) - Testing @MainActor classes (state machines, regression tests)
|