一、Swift Package Manager(以下简称SPM)简介
1.1、SPM 核心特性
1. 原生集成
- Apple 官方支持:从 Xcode 11 开始内置,无需额外安装(对比 CocoaPods 需要 Ruby 环境,Carthage 需要 Homebrew)。
- 与 Xcode 无缝协作 :通过
Package.swift
文件定义依赖,Xcode 可直接解析并下载依赖。
2. 模块化设计
- 基于 Swift 的模块系统 :每个包(Package)是一个独立的模块,支持
target
划分(如库、测试、可执行文件)。 - 跨平台兼容:同一套代码可编译到不同 Apple 平台或 Linux。
3. 依赖管理
- 支持远程依赖:通过 Git 仓库(GitHub/GitLab)引入开源库。
- 支持本地依赖:直接引用本地路径的模块(适合内部开发)。
- 版本控制:支持语义化版本(SemVer)、分支、Commit Hash 等。
4. 资源管理
- 原生支持资源文件 :如
.xcassets
、xib
、storyboard
,通过resources
字段声明。 - 访问方式 :使用
Bundle.module
加载资源(无需手动处理路径)。
5. 命令行工具
-
核心命令:
perlbash swift package init # 初始化新包 swift package resolve # 解析依赖 swift package update # 更新依赖 swift build # 编译项目 swift test # 运行测试
1.2、SPM vs CocoaPods vs Carthage
对比项 | SPM | CocoaPods | Carthage |
---|---|---|---|
官方支持 | ✅ Apple 官方(Xcode 内置) | ❌ 第三方(Ruby 编写) | ❌ 第三方(Swift/Go 编写) |
安装复杂度 | ⚪ 无安装(Xcode 自带) | ⚫ 需安装 Ruby 和 CocoaPods | ⚫ 需安装 Homebrew 或编译 |
依赖原理 | ✅ 直接编译源码 | ⚫ 生成 .xcworkspace 和动态库 |
✅ 编译二进制框架(静态库) |
版本控制 | ✅ 支持 SemVer/分支/Commit | ✅ 支持 SemVer | ✅ 支持 Git Tag |
资源支持 | ✅ 原生支持(Bundle.module ) |
✅ 通过 resources 插件 |
❌ 需手动处理 |
跨平台 | ✅ 支持 macOS/iOS/Linux | ❌ 仅 Apple 平台 | ✅ 支持 Apple 平台 + Linux |
二进制支持 | 不直接支持.a(包一下), 支持xcframework | ✅ 可生成动态库(.framework ) |
✅ 默认生成静态库(.a ) |
学习曲线 | ⚪ 中等(需熟悉 Package.swift ) |
⚪ 中等(需了解 Podfile ) |
⚪ 中等(需了解 Cartfile ) |
适用场景 | ✅ 新项目、模块化开发 | ✅ 遗留项目、需要动态库 | ✅ 追求编译速度、静态库 |
关键差异分析
-
依赖原理:
- CocoaPods :通过修改
project.pbxproj
文件,生成.xcworkspace
,依赖动态库(可能增加启动时间)。 - Carthage:仅编译二进制框架,不修改项目文件,需手动集成到 Xcode。
- SPM:直接编译源码到目标模块,不生成额外文件,与 Xcode 深度集成。
- CocoaPods :通过修改
-
资源管理:
- SPM :原生支持资源文件,通过
Bundle.module
访问。 - CocoaPods :需通过
resources
插件或手动配置COPY_RESOURCES
。 - Carthage:需手动将资源文件拖入项目。
- SPM :原生支持资源文件,通过
-
二进制支持:
- Carthage 默认生成静态库,适合减小应用体积。
- SPM 和 CocoaPods 默认编译源码,但 CocoaPods 可通过
use_frameworks!
生成动态库。
1.3、SPM 适用场景
1. 推荐使用 SPM 的情况
- 新项目开发:从零开始的项目,可充分利用 SPM 的模块化设计。
- 跨平台库:需要同时支持 macOS/iOS/Linux 的 Swift 库。
- Apple 生态开发:与 Xcode 深度集成,避免第三方工具的兼容性问题。
- 追求简洁性:不想处理 Ruby 环境或 Homebrew 依赖。
2. 不推荐使用 SPM 的情况
- 遗留项目迁移:旧项目已使用 CocoaPods,迁移成本较高。
- 需要二进制分发 :如第三方 SDK 仅提供
.framework
文件(需通过 CocoaPods 或手动集成)。 - 复杂依赖冲突:SPM 的依赖解析逻辑较严格,可能不如 CocoaPods 灵活。
1.4、SPM模块引入方式
引入方式有两种:
1、直接通过远程链接导入 2、直接依赖本地模块
1. 远程依赖(通过 Git 仓库导入)
适用场景
- 依赖的模块是开源库(如
Alamofire
、SwiftLint
)。 - 模块托管在 GitHub、GitLab 或其他 Git 服务器上。
- 需要自动获取最新版本或指定版本范围。
配置方法
(1) 在 Package.swift
中声明远程依赖
swift
// swift-tools-version:5.7
import PackageDescription
let package = Package(
name: "MyApp",
dependencies: [
// 方式1:指定版本范围(如 5.6.0 到 6.0.0)
.package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.6.0"),
// 方式2:精确指定版本(如 5.6.1)
.package(url: "https://github.com/SwiftGen/SwiftGen.git", exact: "6.6.0"),
// 方式3:指定分支(如 main 分支,不推荐生产环境使用)
.package(url: "https://github.com/example/repo.git", branch: "main"),
// 方式4:指定 Commit Hash(用于临时修复)
.package(url: "https://github.com/example/repo.git", revision: "a1b2c3d4e5f6"),
],
targets: [
.target(
name: "App",
dependencies: [
.product(name: "Alamofire", package: "Alamofire"),
.product(name: "SwiftGenKit", package: "SwiftGen"),
]
)
]
)
(2) 在 Xcode 中直接添加远程依赖
- 打开 Xcode 项目,选择 File > Add Packages。
- 输入 Git 仓库 URL(如
https://github.com/Alamofire/Alamofire.git
)。 - 选择版本规则(
Up to Next Major
、Exact Version
或Branch
)。 - 点击 Add Package ,Xcode 会自动生成
Package.swift
并下载依赖。
常见问题
Q1: 依赖下载失败(网络问题或仓库不存在)
-
解决方法:
- 检查网络连接,确保能访问 Git 仓库。
- 如果是私有仓库,需配置 SSH 密钥或 GitHub Personal Access Token。
- 在 Xcode 中重置包缓存:File > Reset Package Caches。
Q2: 版本冲突(如多个依赖要求不同版本的同一库)
-
解决方法:
-
在
Package.swift
中显式指定版本:swift.package(url: "https://github.com/example/repo.git", from: "1.0.0"),
-
使用
resolution
字段强制解析版本(Xcode 14+ 支持):swift// 在 Package.swift 的顶层添加 let package = Package( // ... dependencies: [...], // 强制解析版本 resolutions: [ .package(url: "https://github.com/example/repo.git", exact: "1.0.0") ] )
-
Q3: 如何更新依赖到最新版本?
-
方法:
- 运行
swift package update
命令。 - 在 Xcode 中:Product > Clean Build Folder,然后重新编译。
- 运行
2. 本地依赖(直接引用本地模块)
适用场景
- 依赖的模块是本地开发的(如公司内部库、未开源的模块)。
- 需要快速迭代本地代码,无需频繁推送 Git。
- 模块与主工程紧密耦合,不适合远程托管。
配置方法
(1) 在 Package.swift
中声明本地依赖
假设本地模块位于主工程的同级目录 ../LocalModule
:
swift
// swift-tools-version:5.7
import PackageDescription
let package = Package(
name: "MyApp",
dependencies: [
// 本地依赖(相对路径)
.package(path: "../LocalModule"),
// 也可以混合远程依赖
.package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.6.0"),
],
targets: [
.target(
name: "App",
dependencies: [
"LocalModule", // 直接引用本地模块名
.product(name: "Alamofire", package: "Alamofire"),
]
)
]
)
(2) 本地模块的 Package.swift
示例
本地模块也需要是一个有效的 SPM 包,例如:
swift
// swift-tools-version:5.7
import PackageDescription
let package = Package(
name: "LocalModule",
products: [
.library(name: "LocalModule", targets: ["LocalModule"]),
],
targets: [
.target(name: "LocalModule", path: "Sources"),
]
)
(3) 在 Xcode 中添加本地依赖
- 打开 Xcode 项目,选择 File > Add Packages。
- 点击 Add Local ,选择本地模块的
Package.swift
文件。 - Xcode 会自动解析依赖并链接到主工程。
常见问题
Q1: 本地模块路径错误
-
错误示例:
arduinodependency 'LocalModule' not found at '../LocalModule'
-
解决方法:
-
确保
.package(path: "../LocalModule")
的路径是相对于主工程Package.swift
的。 -
如果路径包含空格或特殊字符,用引号包裹路径:
luaswift .package(path: "/path/with spaces/LocalModule")
-
Q2: 本地模块修改后未生效
-
原因:
- Xcode 可能缓存了旧版本。
-
解决方法:
- 清理构建缓存:Product > Clean Build Folder。
- 重新运行
swift package update
。 - 如果使用 Xcode 的 SPM 集成,尝试 File > Reset Package Caches。
Q3: 本地模块如何引用主工程的代码?
-
问题:
- 本地模块和主工程可能存在循环依赖。
-
解决方法:
- 避免循环依赖,将共享代码提取到第三个模块中。
- 如果必须引用,可以使用 Xcode Workspace 结合 SPM(不推荐,复杂度高)。
Q4: 本地模块包含资源文件(如 .xcassets
)
-
解决方法:
-
在本地模块的
Package.swift
中声明资源:swift.target( name: "LocalModule", dependencies: [], resources: [.process("Resources")] // 指定资源目录 )
-
在代码中通过
Bundle.module
加载资源:swiftlet bundle = Bundle.module let image = UIImage(named: "MyImage", in: bundle, with: nil)
-
3. 远程依赖 vs 本地依赖:如何选择?
对比项 | 远程依赖 | 本地依赖 |
---|---|---|
适用场景 | 开源库、稳定版本 | 内部开发、快速迭代 |
版本控制 | 通过 Git 标签/分支管理 | 直接修改代码,无需推送 |
构建速度 | 较慢(需下载) | 快(本地直接引用) |
协作性 | 适合团队共享 | 适合单人或本地开发 |
依赖冲突 | 可能因版本不兼容报错 | 路径错误或循环依赖更常见 |
4. 最佳实践
- 远程依赖 :通过
.package(url:from:)
引入,适合开源库和稳定版本。 - 本地依赖 :通过
.package(path:)
引入,适合快速迭代的内部模块。 - Xcode 集成 :支持通过 GUI 添加依赖,但底层仍依赖
Package.swift
。 - 常见问题:路径错误、版本冲突、缓存问题,通常通过清理缓存或调整路径解决。
- 避免混合管理依赖 :不要在
Package.swift
和Podfile
/Cartfile
中重复声明同一依赖。 - 定期更新依赖 :运行
swift package update
或使用 Xcode > Product > Clean Build Folder 保持依赖最新。
5. 彻底清理缓存
⚠️注意:当你在尝试配置模块已经依赖时,如果没有完全清空缓存,可能导致你正确的代码无法正常编译。
5.1.当前模块改动
sh
swift package update
swift package clean
5.2.Package依赖缓存清理
Xcode的工具栏:File - Packages - Reset Package Caches
5.3.退出Xcode 工程, 重新打开项目 必要时清理项目缓存 ~/Library/Developer/Xcode/DerivedData/你的工程
二、本地模块实操
实践中通常使用现有项目,此文演示先创建一个空OC项目(Swift也一样, 只不过大部分项目壳工程都是OC,兼容性问题也多,方便演示后面一些问题):

1、纯Swift文件
项目路径下添加模块
sh
#本地管理路径
➜ SPMDemo git:(main) ✗ mkdir LocalModules && cd LocalModules
#测试模块1
➜ LocalModules git:(main) ✗ mkdir ModulesA && cd ModulesA
#创建项目模块
➜ ModulesA git:(main) ✗ swift package init --type library
Creating library package: ModulesA
Creating Package.swift
Creating .gitignore
Creating Sources/
Creating Sources/ModulesA/ModulesA.swift
Creating Tests/
Creating Tests/ModulesATests/
Creating Tests/ModulesATests/ModulesATests.swift
swift package init --type
参数
--type
参数用于指定初始化包的类型,支持以下选项:
选项 | 说明 |
---|---|
--type library |
创建一个 库(Library) 类型的包(默认选项,可省略)。 |
--type executable |
创建一个 可执行文件(Executable) 类型的包(包含 main.swift )。 |
--type system-module |
创建一个 系统模块(System Module) 类型的包(用于 C 语言家族的模块)。 |
--type manifest |
仅创建一个空的 Package.swift 清单文件(不生成其他模板文件)。 |
生成的library文件结构:
bash
ModulesA/
├── Package.swift # 包描述文件
├── Sources/
│ └── ModulesA/ # 库源码目录,这里可以添加你的swift模块
│ └── ModulesA.swift
└── Tests/
└── ModulesATests/ # 测试目录
└── ModulesATests.swift
添加:

设置目标工程:

纯swift模块在项目中效果:

⚠️注意:如果你的库源码目录不在Sources/ModulesA下,而是自定义路径(建议使用规范路径),需要设置path:

2、纯OC文件
2.1、模块目录结构
ruby
MyOCModule/
├── Sources/
│ ├── MyOCModule/ # OC 代码目录
│ │ ├── include/ # 公开头文件目录(可选)
│ │ │ └── MyOCModule.h # 模块主头文件
│ │ │ └── module.modulemap # 模块映射文件(关键)
│ │ └── src/ # OC 源文件目录
│ │ └── MyOCClass.m # OC 类实现
│ │ └── MyOCClass.h # OC 类头文件
├── Package.swift # SPM 配置文件
└── README.md # 项目说明(可选)
2.2、关键文件配置
1. module.modulemap
(模块映射文件)
定义 OC 模块的接口,指定头文件路径。示例内容:
swift
module MyOCModule {
header "MyOCModule.h" // 指向模块主头文件
export * // 导出所有头文件内容
}
header
:指定模块的主头文件(如MyOCModule.h
),该文件需包含所有对外公开的头文件。export
:控制头文件的可见性(*
表示导出所有内容)。
2. Package.swift
(SPM 配置文件)
配置 Target 依赖关系及编译路径。示例内容:
swift
// swift-tools-version:5.7
import PackageDescription
let package = Package(
name: "MyOCModule",
products: [
.library(
name: "MyOCModule",
targets: ["MyOCModule"]
),
],
targets: [
.target(
name: "MyOCModule",
path: "Sources/MyOCModule", // OC 代码路径
exclude: ["info.plist"], // 被排除的文件,多个用逗号分隔(如果有就行设置,没有的文件不要写,会有警告文件找不到)
sources: ["src"], // OC源代码路径:path 路径下
publicHeadersPath: "include" // 公开头文件夹
),
]
)
publicHeadersPath
:指定公开头文件目录(如include
),需与module.modulemap
中的路径一致。sources
:指定源文件目录(如src
),包含.m
和.h
文件。

⚠️注意:模块集成后,会以库形式在工程中被依赖,假如你要删除此OC模块,需要确保此处同样被移除,否则编译会报错!!!

3、Swift Package模块 依赖 OC Package 模块
1. 在 Swift 代码中导入 OC 模块
⚠️注意:
这里OC Package 是通过配置
include/module.modulemap
实现被Swift 依赖引用的,另外一种方式后面介绍
1、Package 配置:
swift
// swift-tools-version: 5.10
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "ModulesA",
products: [
.library(
name: "ModulesA",
targets: ["ModulesA"]),
],
dependencies: [
// 依赖地址或者本地资源文件夹地址.
// .package(url: /* package url */, from: "1.0.0"),
.package(path: "../MyOCModule")
],
targets: [
.target(
name: "ModulesA",
dependencies: [
// 依赖OC模块
.product(name: "MyOCModule", package: "MyOCModule")
]
),
.testTarget(
name: "ModulesATests",
dependencies: ["ModulesA"]),
]
)
⚠️注意:上面依赖一个模块时,一个当前Package索引的依赖路径,也就是和 targets 评级的, 而每个不同的子target根据自己的需要依赖不同的模块,比如此处设置依赖的MyOCModule:
swift
// 依赖OC模块
.product(name: "MyOCModule", package: "MyOCModule")
发现product 的name 和 package name 是一样的,那么就可以简写成如下:
swift
.target(
name: "ModulesA",
dependencies: [
"MyOCModule" // 依赖OC模块
]
)
2、在 Swift 文件中,通过 import
语句导入 OC 模块
swift
import MyOCModule // 导入 OC 模块
class SwiftClass {
func useOCMethod() {
let ocObject = MyOCClassPublicA()
ocObject.test() // OC测试方法调用
}
}
4、同一个 OC Package 模块中多个 OC target(类似于.podspec中 subspec)
1.配置 Target 依赖关系
若模块间需相互调用,需在 Package.swift
中配置 Target 依赖关系:
swift
// swift-tools-version: 5.10
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "MyOCModule",
products: [
// Products define the executables and libraries a package produces, making them visible to other packages.
.library(
name: "MyOCModule",
targets: ["MyOCModule"]),
.library(
name: "MyOCModuleB",
targets: ["MyOCModuleB"]),
],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
.target(
name: "MyOCModule",
dependencies: ["MyOCModuleB"], // 依赖 MyOCModuleB
path: "Sources/MyOCModule", // OC 代码路径
exclude: ["info.plist"], // 被排除的文件,多个用逗号分隔(如果有就行设置,没有的文件不要写,会有警告文件找不到)
sources: ["src"], // OC源代码路径:path 路径下
publicHeadersPath: "include"
),
.target(
name: "MyOCModuleB",
dependencies: [],
path: "Sources/MyOCModuleB", // OC 代码路径
sources: ["src"],
publicHeadersPath: "include"
)
]
)
2.目录结构和调用:

⚠️注意:
从上面截图可以看到,在同一个模块内部代码之间一般是直接引用的 #import "MyOCModuleB.h" 那么,如果需要 #import <MyOCModuleB/MyOCModuleB.h> 方式引入呢?在下一节介绍
5、OC Package 依赖另一个 OC Package 模块
前面提到,OC target 通过配置 include/module.modulemap
是可以实现:
1、直接被Swift Package/target 通过import PackageName or targetName 引用
2、或者 OC target 依赖后通过 #import "FileName.h" 引用的
但是 OC Package 之间是需要通过 #import <MyOCModuleB/MyOCModuleB.h> 方式引用头文件的,接下来通过几个步骤介绍:
1、创建一个OC Package:MyOCHello
swift package init --type library MyOCHello
swift
MyOCHello/
├── Sources/
│ ├── MyOCHello/ # OC 代码目录
│ │ ├── include/ # 公开头文件目录
│ │ │ └── MyOCHello.h # 模块主头文件
│ │ └── Core/ # OC 源文件目录
│ │ │ └── MyOCHelloName.m # OC 类实现
│ │ │ └── MyOCHelloName.h # OC 类头文件
│ │ └── Private/ # OC 源文件目录
│ │ └── MyOCHelloAge.m # OC 类实现
│ │ └── MyOCHelloAge.h # OC 类头文件
├── Package.swift # SPM 配置文件
2、设置公开头文件目录
将 MyOCHelloName.h
文件作为 public
头文件,前面示例中,我直接将 MyOCClassPublicA.h
文件拖动到 include
文件夹中。
但是在MyOCHello 模块不准备如此,而是创建MyOCHelloName.h
文件替身 放到include
文件夹中
原因1:此模块我不打算创建
module.modulemap
文件,移走后可能导致MyOCHelloAge.m
无法使用#import "MyOCHelloName.h"
原因2:不希望移动现有工程的文件的头文件
3、通过命令 ln -s
创建替身
:
格式 ln -s
xxx文件夹目录/**.h
xxx文件夹目录/
sh
ln -s ~/Documents/SPMDemo/LocalModules/MyOCHello/Sources/MyOCHello/Core/MyOCHelloName.h ~/Documents/SPMDemo/LocalModules/MyOCHello/Sources/MyOCHello/include/MyOCHello
⚠️注意: 创建替身实践中存在以下几个问题
,导致我项目加载总是失败(以下3种情况创建的替身在我电脑均索引失败😑,可能只是我的电脑问题,列出来希望能少走弯路)
1、右键.h文件创建替身,然后拖动到 include/MyOCHello/ 文件夹下面,替身在Xcode 15中无法编辑,无法索引
2、使用 ln -s 错误命令:ln -s xx/MyOCHelloName.h xx/MyOCHelloName.h
3、文件路径使用的相对路径,拖动替身到其它文件夹中就可能存在无法正常索引情况,请使用绝对路径。
至此,文件夹目录大致是这样的:

或者在终端通过 tree
命令(安装:brew install tree
)生成结构:
js
➜ MyOCHello git:(main) ✗ tree
.
├── Package.swift
├── Sources
│ └── MyOCHello
│ ├── Core
│ │ ├── MyOCHelloName.h
│ │ └── MyOCHelloName.m
│ ├── Private
│ │ ├── MyOCHelloAge.h
│ │ └── MyOCHelloAge.m
│ └── include
│ └── MyOCHello
│ ├── MyOCHello.h
│ └── MyOCHelloName.h -> xxx/Core/MyOCHelloName.h //会显示真身路径
└── Tests
└── MyOCHelloTests
└── MyOCHelloTests.swift
9 directories, 8 files
如果你想展示模块配置信息: 执行命令 swift package describe
js
➜ MyOCHello git:(main) ✗ swift package describe
Name: MyOCHello
Manifest display name: MyOCHello
Path: /Users/xxx/SPMDemo/LocalModules/MyOCHello
Tools version: 5.10
Dependencies:
Platforms:
Products:
Name: MyOCHello
Type:
Library:
automatic
Targets:
MyOCHello
Targets:
Name: MyOCHello
Type: library
C99name: MyOCHello
Module type: ClangTarget
Path: Sources/MyOCHello
Sources:
Core/MyOCHelloName.m
Private/MyOCHelloAge.m
Product memberships:
MyOCHello
4、编辑模块头文件:
MyOCHello.h
js
#ifndef MyOCHello_h
#define MyOCHello_h
// 暴露头文件给外部使用时,使用简括号形式引入,并且对应头文件能在include目录下索引
#import <MyOCHello/MyOCHelloName.h>
#endif /* MyOCHello_h */
MyOCHelloName.h
js
@interface MyOCHelloName : NSObject
+ (void)helloSomeOne:(NSString *)name;
@end
MyOCHelloName.m
js
#import "MyOCHelloName.h"
#import "MyOCHelloAge.h"
@implementation MyOCHelloName
+ (void)helloSomeOne:(NSString *)name {
NSLog(@"MyOCHello, name:%@, age:%@", name, [MyOCHelloName showAge]);
}
@end
5、MyOCModule 依赖 MyOCHello 模块
js
dependencies: [
// Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"),
.package(path: "../MyOCHello"),
],
targets: [
.target(
name: "MyOCModule",
dependencies: [
"MyOCHello"
], // 依赖
等等
6、清理缓存(前面介绍了如何彻底清理缓存)
Xcode的工具栏:File - Packages - Reset Package Caches

重启工程
7、引用并调用
js
#import "MyOCClassPublicA.h"
#import <UIKit/UIKit.h>
// 引用其它OC模块
#import <MyOCHello/MyOCHello.h>
@implementation MyOCClassPublicA
/// 调用MyOCHello模块方法
- (void)OCHelloName {
[MyOCHelloName helloSomeOne:@"Bobo"];
}
@end
6、OC、Swift混编模块
6.1. CocoaPods场景:
在CocoaPods 中存在一些老项目是OC代码为主,后来新增部分Swift代码, OC 和 Swift直接是可以直接相互引用
的。
1、Pods库中代码文件可以 混编
2、Swift可以直接使用当前模块的公开的OC头文件,podspec中设置public路径,最后在-umbrella.h中查看到:

3、而module中的OC文件可以通过
#import <SHMaasUtils/SHMaasUtils-Swift.h>
依赖模块中@objc public
的swift文件,而这个-Swift.h
头文件是自动生成的,不需要手动创建。
6.2. SPM场景:
1、Package库中混编代码文件 不可以 混装,需要单独设置 target
2、Swift可以通过
dependencies
依赖公开的OC头文件(前面已经介绍了)3、Package中的OC target文件目前尝试了,无法通过通过
#import <SHMaasUtils/SHMaasUtils-Swift.h>
依赖模块中@objc public
的swift文件,而这个-Swift.h
头文件是不会自动生成的如果你通过
dependencies
依赖Swift target,那么必然导致依赖循环
,可使用拆分公共文件
作为独立target,协议解耦
, 等方法处理,再使用@import
引用swift文件即可
下面只介绍最复杂的场景
1、如果存在A.h 文件 和 B.swift 文件相互依赖的情况,那么需要将源码先解耦合 最终:A.h依赖C、 B.swift依赖C
2、将Package拆分: OCTarget、SwiftTarget、AdapterTarget
依赖关系1: OCTarget -> (SwiftTarget -> AdapterTarget)
依赖关系2: SwiftTarget -> (OCTarget -> AdapterTarget)
依赖关系3: SwiftTarget -> AdapterTarget
和 OCTarget -> AdapterTarget
⚠️ 1和2场景的混合模块还是很常见,如果完全做到3的关系可能修改业务比较多,影响过大。
下面就以关系1的场景创建示例模块,并且AdapterTarget为OC代码的场景。
6.3. 创建混合模块:
文件目录:
js
➜ MixModule git:(main) ✗ tree
.
├── MixModuleAdapter
│ ├── PersonAdapter.h
│ └── PersonAdapter.m
├── MixModuleOC
│ ├── PersonHomeView.h
│ ├── PersonHomeView.m
│ └── include
│ └── MixModuleOC
│ ├── MixModuleOC.h
│ └── PersonHomeView.h -> /Users/xxx/SPMDemo/LocalModules/MixModule/MixModuleOC/PersonHomeView.h
├── MixModuleSwift
│ └── PersonInfoView.swift
└── Package.swift
6 directories, 8 files
Package.swift
js
// swift-tools-version: 5.10
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "MixModule",
products: [
.library(
name: "MixModuleOC",
targets: ["MixModuleOC"]),
.library(
name: "MixModuleSwift",
targets: ["MixModuleSwift"]),
.library(
name: "MixModuleAdapter",
targets: ["MixModuleAdapter"]),
],
targets: [
.target( // OC业务
name: "MixModuleOC",
dependencies: ["MixModuleSwift"],
path:"MixModuleOC",
sources: [""], // 需要设置
publicHeadersPath: "include"),
.target( // swift 业务
name: "MixModuleSwift",
dependencies: ["MixModuleAdapter"],
path:"MixModuleSwift"
),
.target( // 中间、适配
name: "MixModuleAdapter",
dependencies: [],
path:"MixModuleAdapter",
sources: [""],
publicHeadersPath: ""
)
]
)
1、OC target调用Swift target文件:
MixModuleOC.h (可以对外暴露)
Swift
//
// MixModuleOC.h
//
//
// Created by 聂小波 on 2025/9/10.
//
#ifndef MixModuleOC_h
#define MixModuleOC_h
#import <MixModuleOC/PersonHomeView.h>
#endif /* MixModuleOC_h */
PersonHomeView.m (⚠️ 依赖swift
target 使用 @import
)
swift
//
// PersonHomeView.m
//
//
// Created by 聂小波 on 2025/9/10.
//
#import "PersonHomeView.h"
// 引用swift target
@import MixModuleSwift;
@implementation PersonHomeView
- (void)showInfoView {
NSLog(@"PersonHomeView show");
PersonInfoView *personInfoView = [[PersonInfoView alloc] init];
[personInfoView hello];
}
@end
2、Swift target调用OC target文件:
swift
//
// PersonInfoView.swift
//
//
// Created by 聂小波 on 2025/9/10.
//
import Foundation
import UIKit
// 依赖OC target
import MixModuleAdapter
// 注意:给OC工程或者模块调用需要 @objc public
@objc
public class PersonInfoView: NSObject {
@objc public override init() {
super.init()
}
@objc public func hello() {
let adapter = PersonAdapter()
print("PersonInfoView: 我是", adapter.name())
}
}
3、其它 Package 依赖 MixModule Package 的 MixModuleOC target
js
dependencies: [
// .package(url: /* package url */, from: "1.0.0"),
.package(path: "../SnapKit"),
.package(path: "../MixModule") // 本地模块路径
],
targets: [
.target(
name: "MyOCModule",
dependencies: [
"SHMaasService",
// 依赖 MixModule Package 的 MixModuleOC target
.product(name: "MixModuleOC", package: "MixModule")
],
....
7、依赖 framework 库
如果你的项目必须依赖某个 framework 库
1、如果是 .xcframework 那么不用处理,SPM 直接支持
2、如果是 .framework 需要转 .xcframework
3、如果 .xcframework 放到主工程中,那么可以直接使用
4、如果 .xcframework 在 package 中通过 url 远程加载直接使用
5、如果 .xcframework 在 package 中通过本地文件路径加载,需要先签名
1. MBProgressHUD.framework 转 MBProgressHUD.xcframework

MBProgressHUD文件结构(不含版本):
js
➜ MBProgressHUD.framework
.
├── Headers
│ └── MBProgressHUD.h
├── Info.plist
├── MBProgressHUD
└── Modules
└── module.modulemap
3 directories, 4 files
结构简单,可以直接转xcframework
转换脚本 xcframework.sh:
js
#!/bin/bash
# 检查是否传入了框架路径参数
if [ $# -eq 0 ]; then
echo "未输入framework名称"
exit 1
fi
FRAMEWORK_NAME="$1"
# 创建临时目录
mkdir -p ./iOS-Device ./iOS-Simulator
# 提取 arm64(真机)版本
lipo -extract arm64 "${FRAMEWORK_NAME}.framework/${FRAMEWORK_NAME}" -o "./iOS-Device/${FRAMEWORK_NAME}"
cp -R "${FRAMEWORK_NAME}.framework" "./iOS-Device/${FRAMEWORK_NAME}.framework"
mv "./iOS-Device/${FRAMEWORK_NAME}" "./iOS-Device/${FRAMEWORK_NAME}.framework/"
# 提取 x86_64(模拟器)版本
lipo -extract x86_64 "${FRAMEWORK_NAME}.framework/${FRAMEWORK_NAME}" -o "./iOS-Simulator/${FRAMEWORK_NAME}"
cp -R "${FRAMEWORK_NAME}.framework" "./iOS-Simulator/${FRAMEWORK_NAME}.framework"
mv "./iOS-Simulator/${FRAMEWORK_NAME}" "./iOS-Simulator/${FRAMEWORK_NAME}.framework/"
xcodebuild -create-xcframework \
-framework "./iOS-Device/${FRAMEWORK_NAME}.framework" \
-framework "./iOS-Simulator/${FRAMEWORK_NAME}.framework" \
-output "${FRAMEWORK_NAME}.xcframework"
# 如果是本地SPM调试,需要对每个平台(真机/模拟器)的框架进行签名
codesign --force --sign "Apple Development: Dean Nie (XXS7DPMZ77)" \
--entitlements entitlements.plist \
--timestamp=none \
"${FRAMEWORK_NAME}.xcframework/ios-arm64/${FRAMEWORK_NAME}.framework"
codesign --force --sign "Apple Development: Dean Nie (XXS7DPMZ77)" \
"${FRAMEWORK_NAME}.xcframework/ios-arm64_x86_64-simulator/${FRAMEWORK_NAME}.framework"
先授权脚本,再使用: xcframework.sh 和 MBProgressHUD.framework在同一目录下
sh
chmod +x xcframework.sh
./xcframework.sh MBProgressHUD
2. FMDB.framework 转 FMDB.xcframework

framework文件结构(含版本):
js
➜ FMDB.framework
.
├── FMDB -> Versions/Current/FMDB
├── Headers -> Versions/Current/Headers
├── Modules
│ └── module.modulemap
└── Versions
├── A
│ ├── FMDB
│ └── Headers
│ ├── FMDB.h
│ ├── FMDatabase.h
│ ├── FMDatabaseAdditions.h
│ ├── FMDatabasePool.h
│ ├── FMDatabaseQueue.h
│ └── FMResultSet.h
└── Current -> A
7 directories, 9 files
通过文件结构可以发现,framework 真实二进制文件 可以通过 FMDB.framework/Versions/Current/
路径查找
⚠️ 注意:在 SPM 中xcframework 文件结构测试下来不支持 版本管理结构。

可以自己动手修改脚本将文件重新梳理
3. MBProgressHUD.xcframework 放在主工程中使用
修改 linkerSettings 添加依赖即可:
js
targets: [
.target(
name: "MixModuleOC",
dependencies: ["MixModuleSwift"],
path:"MixModuleOC",
sources: [""],
publicHeadersPath: "include",
linkerSettings: [
.linkedFramework("MBProgressHUD")
]
),
4. MBProgressHUD.xcframework 放在package中
创建一个包含多个库的Package:FMLibs
目录结构如下:

js
➜ FMLibs git:(main) ✗ tree
.
├── Package.swift
└── binaryLibs
└── MBProgressHUD.xcframework
Package 配置如下:
js
// swift-tools-version: 5.10
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "FMLibs",
products: [
.library(
name: "FMLibs",
targets: [
"MBProgressHUD",
// "FMDB"
]
),
],
targets: [
.binaryTarget(name: "MBProgressHUD",path: "binaryLibs/MBProgressHUD.xcframework"),
// .binaryTarget(name: "FMDB",path: "binaryLibs/FMDB.xcframework"),
]
)
5. MixModule 依赖 FMLibs 并使用
MixModule Package.swift
js
dependencies: [
.package(path: "../FMLibs")
],
targets: [
.target(
name: "MixModuleOC",
dependencies: [
"MixModuleSwift",
"FMLibs"
],
path:"MixModuleOC",
sources: [""],
publicHeadersPath: "include",
linkerSettings: [
]
),
显示HUD(直接调用演示)
js
#import "PersonHomeView.h"
@import MixModuleSwift;
//binaryTarget
#import <MBProgressHUD/MBProgressHUD.h>
@implementation PersonHomeView
/// 测试HUD提示
- (void)showHUD {
[MBProgressHUD showHUDAddedTo:[UIApplication sharedApplication].keyWindow animated:YES];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[MBProgressHUD hideHUDForView:[UIApplication sharedApplication].keyWindow animated:YES];
});
}
- (void)showInfoView {
[self showHUD];
NSLog(@"PersonHomeView show");
PersonInfoView *personInfoView = [[PersonInfoView alloc] init];
[personInfoView hello];
}
@end
运行app:

6.包含.bundle 资源的 MJRefresh.framework 库
SPM支持bundle资源文件,直接使用前面脚本转 MJRefresh.xcframework

FMLibs Package.swift
js
// swift-tools-version: 5.10
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "FMLibs",
products: [
.library(
name: "FMLibs",
targets: [
"MBProgressHUD",
"MJRefresh"
]
),
],
targets: [
.binaryTarget(name: "MJRefresh",path: "binaryLibs/MJRefresh.xcframework"),
.binaryTarget(name: "MBProgressHUD",path: "binaryLibs/MBProgressHUD.xcframework")
]
)
在ViewController使用:
js
import MJRefresh
class RefreshViewController: UIViewController {
private let tableView = UITableView()
private var dataSource = [String](repeating: "初始数据", count: 10)
override func viewDidLoad() {
super.viewDidLoad()
setupTableView()
configureRefreshHeader()
}
private func setupTableView() {
tableView.frame = view.bounds
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
tableView.dataSource = self
view.addSubview(tableView)
}
private func configureRefreshHeader() {
// 创建自定义刷新头
let header = MJRefreshNormalHeader { [weak self] in
self?.loadNewData()
}
guard let header else { return }
// 本地化文本设置
header.setTitle("下拉刷新", for: .idle)
header.setTitle("释放立即刷新", for: .pulling)
header.setTitle("加载中...", for: .refreshing)
header.lastUpdatedTimeLabel?.isHidden = true
tableView.mj_header = header
}
private func loadNewData() {
// 模拟网络请求
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
self.dataSource.insert("新增数据 \(Date())", at: 0)
self.tableView.reloadData()
self.tableView.mj_header?.endRefreshing()
}
}
}
extension RefreshViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return dataSource.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = dataSource[indexPath.row]
return cell
}
}
运行:

8、集成 .a 静态库
SPM 中无法直接使用 .a 文件,需要包在 xcframework 形式文件夹中访问
接下来演示如何将 libffi.a 转 ffi.xcframework
1、libffi 的源码文件如下:
js
➜ libffi-source git:(main) ✗ tree
.
├── ffi.h
├── ffi_arm.h
├── ffi_arm64.h
├── ffi_i386.h
├── ffi_x86_64.h
├── ffitarget.h
├── ffitarget_arm.h
├── ffitarget_arm64.h
├── ffitarget_i386.h
├── ffitarget_x86_64.h
└── libffi.a
2、将所以的.h 文件都放入 Headers 文件夹中
3. 检查 .a
库支持的架构
js
➜ lib git:(main) ✗ lipo -info libffi.a
Architectures in the fat file: libffi.a are: armv7 i386 x86_64 arm64
4. 拆分 .a 库
sh
# 提取 arm64(真机)
➜ lib git:(main) ✗ lipo -extract arm64 libffi.a -o ffi-arm64.a
# 检查提取结果是否正确
➜ lib git:(main) ✗ lipo -info ffi-arm64.a
Architectures in the fat file: ffi-arm64.a are: arm64
# 提取 x86_64(Intel 模拟器)
➜ lib git:(main) ✗ lipo -extract x86_64 libffi.a -o ffi-x86_64.a
# 检查提取结果是否正确
➜ lib git:(main) ✗ lipo -info ffi-x86_64.a
Architectures in the fat file: ffi-x86_64.a are: x86_64
➜ lib git:(main) ✗
5.重新组合成 xcframework 文件结构,我命名为 ffi.xcframework
创建文件夹 ffi.xcframework 并按照以下通用格式移动文件:

⚠️ 注意:我在前面 生成的 ffi-arm64.a
和 ffi-x86_64.a
文件最后都更改名称为 ffi
(需要删除 .a
后缀)
6.依赖和使用
使用 ffi 调用一个 乘法的 c 函数:
js
#import "PersonHomeView.h"
//binaryTarget
#import <ffi/ffi.h>
@implementation PersonHomeView
/// 乘法功能的C函数
int cFuncMultiply(int a, int b) {
return a * b;
}
/// 测试 ffi 调用 c 函数
- (void)libffiTest {
//1.定义参数类型数组(告诉 libffi 参数的类型)
ffi_type **argTypes;
argTypes = malloc(sizeof(ffi_type *) * 2);
argTypes[0] = &ffi_type_sint;
argTypes[1] = &ffi_type_sint;
//2.定义返回值类型(告诉 libffi 返回值的类型)
ffi_type *retType = &ffi_type_sint; // 返回值是 int
//3.初始化 CIF(Call Interface,调用接口)
ffi_cif cif; // CIF 存储函数调用的 ABI、参数类型、返回值类型等信息
ffi_prep_cif(&cif, FFI_DEFAULT_ABI, 2, retType, argTypes);
//4.准备参数值(实际调用时传入的参数)
void **args = malloc(sizeof(void *) * 2);
int x = 3, y = 7;
args[0] = &x; // 第一个参数是 x 的地址
args[1] = &y; // 第二个参数是 y 的地址
int ret; // 存储返回值
//5.动态调用函数
ffi_call(&cif, (void(*)(void))cFuncMultiply, &ret, args);
// 打印结果
NSLog(@"libffi:乘法功能的C函数调用结果: %d", ret);
// 释放内存
free(argTypes);
free(args);
}
@end
打印结果:
js
SPMDemo[10519:7206102] libffi:乘法功能的C函数调用结果: 21
9、资源加载
资源文件位置
SPM 要求资源文件放置在包的 Resources 目录下(或通过目录结构隐式标记为资源)。例如:
js
MyPackage/
├── Sources/
│ └── MyFramework/
│ ├── Resources/ # 显式资源目录
│ │ └── image.png
│ └── MyClass.swift
└── Package.swift
或通过文件扩展名隐式标记(如 .xcassets、.lproj 等),SPM 会自动识别为资源。
Package.swift 配置
在包的清单文件中,需通过 targets 的 resources 字段显式声明资源文件或目录:
js
swift
targets: [
.target(
name: "MyFramework",
exclude: ["info.plist"], // 排除的文件
resources: [
.process("Resources/**"]), // 递归包含 Resources 目录下所有文件
.copy() //资源按原样复制
]
)
]
.process(), // 大图等资源会被复制到构建产物中,并可通过 Bundle 访问。
.copy(), // 资源按原样复制,适用于非文本文件(如二进制数据)
10、如何管理工程依赖
1、GUI界面手动管理
通过GUI界面的方式手动的一个一个的进行添加、删除不是一个明智的选择。
你可以通过脚本管理等方式放开你的双手。
2、模块 SPMManager
也可以创建一个空壳模块专门管理依赖
比如,创建一个模块:SPMManager
主工程手动添加 SPMManager 依赖
本地模块示意:
其它的模块都在 SPMManager 中 Package.swift 里配置。
你也可以通过脚本快速修改 Package.swift 文件,比修改工程可是要方便得多。