# Macos Spm App Structure

> 创建或重构 macOS SwiftUI App，使业务源码、资源和测试由本地 Swift Package Manager 模块管理。适用于新建 macOS App、迁移已有 Xcode 工程、整理源码目录、迁移 Localizable.strings、Assets.xcassets 或 AppIcon，并确保编译后的 App 能加载资源。

- Skill: `lingmacker/macos-spm-app-structure` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add lingmacker/macos-spm-app-structure`
- Raw SKILL.md: https://api.skillmd.com/api/skills/lingmacker/macos-spm-app-structure/raw
- Safety review: pending (external: skill-scanner PASS, skillspector CAUTION)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: lingmacker (https://skillmd.com/u/lingmacker)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/lingmacker/macos-spm-app-structure

---


# macOS App SPM 结构

将 macOS App 拆成一个薄 App 壳和一个本地 SPM 模块。先读取现有工程、`project.pbxproj`、Scheme、资源引用和测试配置，再修改。不要只移动文件；必须验证生成的 App 包内容。

## 目标结构

```text
MyMacApp/
├── .gitignore
├── MyMacApp.xcodeproj
├── MyMacApp/
│   └── MyMacAppApp.swift
└── Packages/
    └── AppModules/
        ├── Package.swift
        ├── Sources/
        │   ├── Core/
        │   ├── Models/
        │   ├── Views/
        │   ├── Resources/
        │   │   └── Assets.xcassets
        │   └── ...
        └── Tests/
```

按领域复用已有目录，不为单个文件创建空目录。

## Package.swift

当资源位于 `Sources` 外、但必须保持模板结构时，将 target 根设为包根，并限制源码目录：

```swift
// swift-tools-version: 6.2
import PackageDescription

let package = Package(
  name: "AppModules",
  defaultLocalization: "en",
  platforms: [.macOS(.v26)],
  products: [
    .library(name: "AppModules", targets: ["AppModules"])
  ],
  targets: [
    .target(
      name: "AppModules",
      path: ".",
      exclude: ["Tests"],
      sources: ["Sources"],
      resources: [.process("Resources")]
    ),
    .testTarget(
      name: "AppModulesTests",
      dependencies: ["AppModules"]
    )
  ]
)
```

如果资源放在 `Sources/AppModules/Resources`，可以使用默认 target 路径；但最终目录模板优先，使用上面的包根配置。

资源本地化时必须设置 `defaultLocalization`。不要把 `Resources` 作为普通源码目录，也不要把 `Tests` 误编译进应用 target。

## App 壳

`MyMacApp/MyMacAppApp.swift` 只保留 AppKit 生命周期适配和 Scene 入口：

```swift
import SwiftUI
import AppModules

@main
struct MyMacAppApp: App {
  @State private var model = AppModel()

  var body: some Scene {
    WindowGroup {
      ContentView()
        .environment(model)
    }
  }
}
```

模块内被 App 壳直接使用的类型和初始化器必须是 `public`。业务实现放在 `Packages/AppModules/Sources`，不要把业务文件重新加入 App target 的 Sources 阶段。

## Xcode 集成

将 `Packages/AppModules` 作为本地 Swift Package 加入工程，并把 `AppModules` product 链接到 App target 的 Frameworks/Package Products。App target 的 Sources 阶段只保留 `MyMacAppApp.swift`。

资源引用必须同时满足两个运行时需求：

1. SPM target 需要 `.process("Resources")`，资源进入 `AppModules_AppModules.bundle`。
2. SwiftUI 的 `Text("key")`、`LocalizedStringKey` 和 App 图标默认从 App 主 Bundle 查找。若代码使用这些 API，不要只依赖 SPM 资源 Bundle；把 `Packages/AppModules/Resources` 中需要主 Bundle 查找的 `Localizable.strings`、Assets 或 AppIcon 也加入 App target 的 Resources/Asset Catalog 阶段。

这是避免“编译成功但运行时资源找不到”的固定规则。不要清空 App target 的 Resources 阶段。更换为 `Bundle.module` 的显式资源访问可以避免复制，但必须同步改造所有调用点；除非项目已经统一使用该方式，否则优先保持主 Bundle 资源副本。

本地化文件仍放在：

```text
Packages/AppModules/Resources/en.lproj/Localizable.strings
Packages/AppModules/Resources/zh-Hans.lproj/Localizable.strings
```

在 Xcode 工程中引用它们时，引用路径必须指向 `Packages/AppModules/Resources/...`，不能继续指向旧的 `MyMacApp/Resources`。

## 迁移步骤

1. 读取工程文件，列出 App Sources、Resources、Frameworks、Tests 和 Scheme 引用。
2. 在项目根目录创建 `.gitignore`，忽略 macOS、Xcode 和 Swift 本地构建产物。
3. 创建 `Packages/AppModules/Package.swift`。
4. 将业务 Swift 文件迁移到 `Sources/Core`、`Models`、`Views`、`Services` 等目录。
5. 将本地化、图片、Asset Catalog、图标迁移到 `Packages/AppModules/Resources`。
6. 将测试迁移到 `Packages/AppModules/Tests`，改为 `@testable import AppModules`。
7. 保留 App 入口在 `MyMacApp/MyMacAppApp.swift`，改为 `import AppModules`。
8. 清理旧 App Sources 和旧 Tests 的 PBX 引用，避免文件重复编译。
9. 检查 App target 的资源阶段，补上来自 `Packages/AppModules/Resources` 的主 Bundle 资源引用。
10. 更新 Makefile、README 和 Scheme 中的测试命令；测试以 `swift test --package-path Packages/AppModules` 为准。

## 验证

至少执行：

```sh
swift test --package-path Packages/AppModules
xcodebuild -project MyMacApp.xcodeproj -scheme MyMacApp \
  -configuration Debug -derivedDataPath /tmp/mymacapp-build build
```

使用干净 DerivedData 验证资源，不要依赖旧构建产物：

```sh
rm -rf /tmp/mymacapp-build
xcodebuild -project MyMacApp.xcodeproj -scheme MyMacApp \
  -derivedDataPath /tmp/mymacapp-build build
find /tmp/mymacapp-build/Build/Products/Debug/MyMacApp.app/Contents/Resources \
  -name Localizable.strings -o -name Assets.car
```

必须同时检查：

- `AppModules_AppModules.bundle` 存在并包含处理后的资源。
- App 主 Bundle 包含代码默认查找的 `Localizable.strings` 和 Asset Catalog 产物。
- App 图标存在于主 App Bundle 的图标产物中。
- `swift test` 和 Xcode App build 都成功。

不要用“编译成功”代替资源验证。资源问题通常只在干净构建或运行时暴露。

