macOS App SPM 结构
将 macOS App 拆成一个薄 App 壳和一个本地 SPM 模块。先读取现有工程、project.pbxproj、Scheme、资源引用和测试配置,再修改。不要只移动文件;必须验证生成的 App 包内容。
目标结构
MyMacApp/
├── .gitignore
├── MyMacApp.xcodeproj
├── MyMacApp/
│ └── MyMacAppApp.swift
└── Packages/
└── AppModules/
├── Package.swift
├── Sources/
│ ├── Core/
│ ├── Models/
│ ├── Views/
│ ├── Resources/
│ │ └── Assets.xcassets
│ └── ...
└── Tests/
按领域复用已有目录,不为单个文件创建空目录。
Package.swift
当资源位于 Sources 外、但必须保持模板结构时,将 target 根设为包根,并限制源码目录:
// 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 入口:
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。
资源引用必须同时满足两个运行时需求:
- SPM target 需要
.process("Resources"),资源进入AppModules_AppModules.bundle。 - 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 资源副本。
本地化文件仍放在:
Packages/AppModules/Resources/en.lproj/Localizable.strings
Packages/AppModules/Resources/zh-Hans.lproj/Localizable.strings
在 Xcode 工程中引用它们时,引用路径必须指向 Packages/AppModules/Resources/...,不能继续指向旧的 MyMacApp/Resources。
迁移步骤
- 读取工程文件,列出 App Sources、Resources、Frameworks、Tests 和 Scheme 引用。
- 在项目根目录创建
.gitignore,忽略 macOS、Xcode 和 Swift 本地构建产物。 - 创建
Packages/AppModules/Package.swift。 - 将业务 Swift 文件迁移到
Sources/Core、Models、Views、Services等目录。 - 将本地化、图片、Asset Catalog、图标迁移到
Packages/AppModules/Resources。 - 将测试迁移到
Packages/AppModules/Tests,改为@testable import AppModules。 - 保留 App 入口在
MyMacApp/MyMacAppApp.swift,改为import AppModules。 - 清理旧 App Sources 和旧 Tests 的 PBX 引用,避免文件重复编译。
- 检查 App target 的资源阶段,补上来自
Packages/AppModules/Resources的主 Bundle 资源引用。 - 更新 Makefile、README 和 Scheme 中的测试命令;测试以
swift test --package-path Packages/AppModules为准。
验证
至少执行:
swift test --package-path Packages/AppModules
xcodebuild -project MyMacApp.xcodeproj -scheme MyMacApp \
-configuration Debug -derivedDataPath /tmp/mymacapp-build build
使用干净 DerivedData 验证资源,不要依赖旧构建产物:
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 都成功。
不要用“编译成功”代替资源验证。资源问题通常只在干净构建或运行时暴露。