developing-ios-apps

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

iOS App Development

iOS应用开发

Build, configure, and deploy iOS applications using XcodeGen and Swift Package Manager.
使用XcodeGen和Swift Package Manager构建、配置和部署iOS应用。

Critical Warnings

重要警告

IssueCauseSolution
"Library not loaded: @rpath/Framework"XcodeGen doesn't auto-embed SPM dynamic frameworksBuild in Xcode GUI first (not xcodebuild). See Troubleshooting
xcodegen generate
loses signing
Overwrites project settingsConfigure in
project.yml
target settings, not global
Command-line signing failsFree Apple ID limitationUse 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
问题原因解决方案
"Library not loaded: @rpath/Framework"XcodeGen不会自动嵌入SPM动态框架先在Xcode图形界面中构建(不要使用xcodebuild)。查看故障排除
xcodegen generate
丢失签名配置
覆盖了项目设置
project.yml
的target设置中配置,而非全局设置
命令行签名失败免费Apple ID限制使用Xcode图形界面或付费开发者账号(99美元/年)
"Cannot be set when automaticallyAdjustsVideoMirroring is YES"未禁用自动调整就设置
isVideoMirrored
先设置
automaticallyAdjustsVideoMirroring = false
。查看相机

Quick Reference

快速参考

TaskCommand
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 generate
构建模拟器版本
xcodebuild -destination 'platform=iOS Simulator,name=iPhone 17' build
构建设备版本(需付费账号)
xcodebuild -destination 'platform=iOS,name=DEVICE' -allowProvisioningUpdates build
清理DerivedData
rm -rf ~/Library/Developer/Xcode/DerivedData/PROJECT-*
查找设备名称
xcrun xctrace list devices

XcodeGen Configuration

XcodeGen配置

Minimal project.yml

最简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
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
undefined
个人(免费)账号:仅在Xcode图形界面中可用。命令行构建需要付费账号。
yaml
undefined

In target settings

在target设置中

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
settings: base: CODE_SIGN_STYLE: Automatic DEVELOPMENT_TEAM: TEAM_ID # 从Xcode → 设置 → 账号中获取

**获取Team ID**:
```bash
security find-identity -v -p codesigning | head -3

iOS Version Compatibility

iOS版本兼容性

API Changes by Version

各版本API变化

iOS 17+ OnlyiOS 16 Compatible
.onChange { old, new in }
.onChange { new in }
ContentUnavailableView
Custom VStack
AVAudioApplication
AVAudioSession
@Observable
macro
@ObservableObject
SwiftDataCoreData/Realm
仅iOS 17+支持兼容iOS 16
.onChange { old, new in }
.onChange { new in }
ContentUnavailableView
自定义VStack
AVAudioApplication
AVAudioSession
@Observable
@ObservableObject
SwiftDataCoreData/Realm

Lowering Deployment Target

降低部署目标版本

  1. Update
    project.yml
    :
yaml
deploymentTarget:
  iOS: "16.0"
  1. 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
  1. Regenerate:
    xcodegen generate
  1. 更新
    project.yml
    :
yaml
deploymentTarget:
  iOS: "16.0"
  1. 修复不兼容的API:
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
  1. 重新生成项目:
    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
  1. 通过USB连接设备
  2. 在设备上信任此电脑
  3. 在Xcode中:设置 → 账号 → 添加Apple ID
  4. 在方案下拉菜单中选择设备
  5. 运行(
    Cmd + R
  6. 在设备上:设置 → 通用 → VPN与设备管理 → 信任

Command-line Build (requires paid account)

命令行构建(需付费账号)

bash
xcodebuild \
  -project App.xcodeproj \
  -scheme App \
  -destination 'platform=iOS,name=DeviceName' \
  -allowProvisioningUpdates \
  build
bash
xcodebuild \
  -project App.xcodeproj \
  -scheme App \
  -destination 'platform=iOS,name=DeviceName' \
  -allowProvisioningUpdates \
  build

Common Issues

常见问题

ErrorSolution
"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 listedReconnect USB, trust computer on device, restart Xcode
DerivedData won't deleteClose Xcode first:
pkill -9 Xcode && rm -rf ~/Library/Developer/Xcode/DerivedData/PROJECT-*
错误解决方案
"Library not loaded: @rpath/Framework"SPM动态框架未嵌入。先在Xcode图形界面中构建,之后命令行构建即可正常工作
"No Account for Team"在Xcode设置 → 账号中添加Apple ID
"Provisioning profile not found"免费账号限制。使用Xcode图形界面或购买付费账号
设备未列出重新连接USB,在设备上信任电脑,重启Xcode
DerivedData无法删除先关闭Xcode:
pkill -9 Xcode && rm -rf ~/Library/Developer/Xcode/DerivedData/PROJECT-*

Free vs Paid Developer Account

免费与付费开发者账号对比

FeatureFree Apple IDPaid ($99/year)
Xcode GUI builds
Command-line builds
App validity7 days1 year
App Store
CI/CD
功能免费Apple ID付费(99美元/年)
Xcode图形界面构建
命令行构建
应用有效期7天1年
App Store发布
CI/CD支持

SPM Dependencies

SPM依赖

SPM Dynamic Framework Not Embedded

SPM动态框架未嵌入

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
undefined
根本原因:XcodeGen不会为SPM动态框架(如RealmSwift、Realm)生成「嵌入框架」构建阶段。应用构建成功但启动时崩溃,报错:
dyld: Library not loaded: @rpath/RealmSwift.framework/RealmSwift
  Referenced from: /var/containers/Bundle/Application/.../App.app/App
  Reason: image not found
问题原因:
  • 静态框架(大多数SPM包)会链接到二进制文件中 - 无需嵌入
  • 动态框架(RealmSwift等)必须复制到应用包中
  • XcodeGen会生成链接阶段,但不会为SPM包生成嵌入阶段
  • 在project.yml中设置
    embed: true
    会导致构建错误(XcodeGen限制)
修复方案(手动操作,每个项目仅需一次):
  1. 在Xcode图形界面中打开项目
  2. 选择target → 通用 → 框架、库和嵌入式内容
  3. 找到动态框架(RealmSwift)
  4. 将「不嵌入」改为「嵌入并签名」
  5. 先在Xcode图形界面中构建并运行
手动修复后:命令行构建(
xcodebuild
)将可以正常工作,因为Xcode会将嵌入设置保留在project.pbxproj中。
识别动态框架:
bash
undefined

Check if a framework is dynamic

检查框架是否为动态

file ~/Library/Developer/Xcode/DerivedData/PROJECT-*/Build/Products/Debug-iphoneos/FRAMEWORK.framework/FRAMEWORK
file ~/Library/Developer/Xcode/DerivedData/PROJECT-*/Build/Products/Debug-iphoneos/FRAMEWORK.framework/FRAMEWORK

Dynamic: "Mach-O 64-bit dynamically linked shared library"

动态:"Mach-O 64-bit dynamically linked shared library"

Static: "current ar archive"

静态:"current ar archive"

undefined
undefined

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
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  # 当包包含多个产品时,显式指定产品名称

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.
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
不要清理全局SPM缓存
~/Library/Caches/org.swift.swiftpm
)。重新下载速度很慢。

Camera / AVFoundation

相机 / 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
    ?
  1. 权限:是否已在Info.plist中添加
    NSCameraUsageDescription
  2. 设备:是否在真实设备上运行,而非模拟器?
  3. 会话运行
    session.startRunning()
    是否在后台线程调用?
  4. 视图尺寸:UIViewRepresentable是否有非零边界?
  5. 视频镜像:在设置
    isVideoMirrored
    之前是否已禁用
    automaticallyAdjustsVideoMirroring

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
重要提示:必须先禁用自动调整,再设置手动镜像:
swift
// 错误 - 会崩溃并提示"Cannot be set when automaticallyAdjustsVideoMirroring is YES"
connection.isVideoMirrored = true

// 正确 - 先禁用自动调整
connection.automaticallyAdjustsVideoMirroring = false
connection.isVideoMirrored = true

UIViewRepresentable Sizing Issue

UIViewRepresentable尺寸问题

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()
}
ZStack中的UIViewRepresentable可能会有零边界。使用显式框架修复:
swift
// 错误:UIViewRepresentable在ZStack中可能会获得零尺寸
ZStack {
    CameraPreviewView(session: session)  // 可能不可见!
    OtherContent()
}

// 正确:显式设置尺寸
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
添加日志以跟踪相机流程:
swift
import os
private let logger = Logger(subsystem: "com.app", category: "Camera")

func start() async {
    logger.info("start() called, isRunning=\(self.isRunning)")
    // ... 配置代码 ...
    logger.info("session.startRunning() completed")
}

// 针对CGRect(未遵循CustomStringConvertible)
logger.info("bounds=\(NSCoder.string(for: self.bounds))")
在Console.app中按子系统过滤日志。
详细相机实现:查看references/camera-avfoundation.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/xcodegen-full.md - 完整的project.yml选项
  • references/swiftui-compatibility.md - iOS版本API差异
  • references/camera-avfoundation.md - 相机预览调试
  • references/testing-mainactor.md - 测试@MainActor类(状态机、回归测试)