在 Swift 中使用依赖注入Dependency Injection(DI)
了解如何在 Swift 开发中使用依赖注入的方法来创建类。

依赖注入乍听之下可能显得复杂,但其实它是一个简单的概念。可以想象成一段代码需要依赖另一段代码才能正常运行。与其在内部创建这个依赖,我们选择从外部提供它,这个过程就被称为“注入”依赖。
下面通过一个实际的例子,理解什么是依赖注入,以及为什么推荐采用依赖注入。
单一职责原则
假设我们需要实现登陆豆瓣并获取用户数据的逻辑。
这里面会涉及到两个部分:用户认证和数据获取。
单一职责原则推荐将认证和数据获取分开,因为他们是两个不同的关注点。
因此,我们可以创建两个类,每个类专注于自己的功能领域:
DoubanAuthManager()
:负责实现登陆流程、登陆状态、认证信息以及用户基本信息的存储。DoubanManager()
:负责获取用户的相关数据,例如收藏的电影、数据等。
在 DoubanAuthManager
中创建用户登陆的方法:
@Observable
@MainActor
class DoubanAuthManager {
var isLoggedIn: Bool = false
var cookies: [HTTPCookie] = []
// 添加用户信息字段
var userId: String = ""
var userName: String = ""
var userAvatar: String = ""
var userProfileUrl: String = ""
// 保存登录状态
func saveLoginStatus() {
// 将 cookies 保存到用户默认值
// 保存用户信息
}
// 检查登录状态 - 更严格的检测
func checkLoginStatus() -> Bool {
// 检查是否有登录状态标记
}
// 清除登录状态
func clearLoginStatus() {
}
// 获取用户信息
func fetchUserInfo() async {
// 检查是否已登录
}
}
在 DoubanManager
中,创建获取豆瓣数据的方法:
@Observable
@MainActor
class DoubanManager {
// 获取用户收藏的电影
func loadMovies(forceRefresh: Bool = false) async {
}
// 获取用户收藏的书籍
func loadBooks(forceRefresh: Bool = false) async {
}
// 获取正在热映的电影
private func fetchNowPlayingMovies() async {
}
}
理解什么是代码依赖
在实际使用中,有一个问题:DoubanManager
中的 func
,需要访问认证信息才能工作,但用户认证信息存放在 DoubanAuthManager
中。
也就是说,DoubanManager 需要依赖 DoubanAuthManager 中的代码才能够顺利运行。
代码依赖的三种解决方案
解决上面这个问题有三种方案:
- 在内部创建依赖对象
- 使用依赖注入(推荐方式)
- 通过参数传递(无状态依赖)
在内部创建依赖对象
在 DoubanManager 内部直接创建 DoubanAuthManager 实例:
@Observable
class DoubanManager {
// 内部创建依赖
var authManager = DoubanAuthManager()
func fetchUserFavoriteMovies() async -> [DoubanMovie] {
// 直接使用内部创建的 authManager
guard authManager.isLoggedIn, !authManager.userId.isEmpty else {
return []
}
// ... 其余实现
}
}
// 使用时
let manager = DoubanManager()
这种方式会造成了强耦合,难以测试。不符合 SwiftUI 的最佳实践,不推荐使用。
通过参数传递(无状态依赖)
另一种方法是让 DoubanManager 和 DoubanAuthManager 完全独立,而不是在 DoubanManager 中注入 authManager。
独立模型意味着:
@Observable class DoubanManager { ... } // 不持有 authManager 引用
@Observable class DoubanAuthManager { ... } // 独立的认证管理器
然后在视图中分别注入:
struct ContentView: View {
@Environment(DoubanManager.self) private var doubanManager
@Environment(DoubanAuthManager.self) private var authManager
// 在需要时分别使用
}
这种方式在调用具体方法时,通过手动传递参数来解决依赖:
@Observable
class DoubanManager {
// 不持有 authManager 引用
func fetchUserFavoriteMovies(isLoggedIn: Bool, userId: String) async -> [DoubanMovie] {
guard isLoggedIn, !userId.isEmpty else {
return []
}
// 使用传入的参数
}
}
优势是完全解耦,非常易于测试。但方法参数可能变得冗长。
使用依赖注入(推荐方式)
依赖注入(Dependency Injection,简称DI)是一种软件设计模式,它的核心思想是:一个类不应该自己创建它所依赖的对象,而应该从外部接收这些依赖。
简单来说,依赖注入就是将一个对象所需要的其他对象(依赖)从外部传入,而不是在对象内部创建这些依赖。
@Observable
class DoubanManager {
// 从外部接收依赖
let authManager: DoubanAuthManager
// 构造函数注入
init(authManager: DoubanAuthManager) {
self.authManager = authManager
}
func fetchUserFavoriteMovies() async -> [DoubanMovie] {
guard authManager.isLoggedIn, !authManager.userId.isEmpty else {
return []
}
// ... 其余实现
}
}
// 使用时
let authManager = DoubanAuthManager()
let manager = DoubanManager(authManager: authManager)
提高代码的可测试性
不使用依赖注入:
// 测试困难,无法模拟不同的认证状态
func testFetchUserFavorites() {
let manager = DoubanManager()
// 无法控制内部的 authManager 状态
// ...
}
使用依赖注入:
func testFetchUserFavorites() {
// 创建模拟的认证管理器
let mockAuth = MockDoubanAuthManager()
mockAuth.isLoggedIn = true
mockAuth.userId = "test123"
// 使用模拟对象
let manager = DoubanManager(authManager: mockAuth)
// 现在可以控制认证状态并测试不同情况
let movies = await manager.fetchUserFavoriteMovies()
XCTAssertEqual(movies.count, 5)
}
降低组件间的耦合度
不使用依赖注入:
DoubanManager
与具体的DoubanAuthManager
实现紧密耦合- 无法替换为其他认证实现,比如测试版本或不同的认证方式
使用依赖注入:
// 可以定义认证接口
protocol AuthenticationProvider {
var isLoggedIn: Bool { get }
var userId: String { get }
// ...其他必要方法
}
// DoubanManager 依赖于接口而非具体实现
class DoubanManager {
let authProvider: AuthenticationProvider
init(authProvider: AuthenticationProvider) {
self.authProvider = authProvider
}
}
// 可以轻松替换不同的认证实现
let manager1 = DoubanManager(authProvider: DoubanAuthManager())
let manager2 = DoubanManager(authProvider: MockAuthProvider())
let manager3 = DoubanManager(authProvider: AppleAuthProvider())
支持单一实例共享(单例模式的更好替代)
@main
struct FilmoApp: App {
// 在应用入口创建共享实例
@State private var authManager = DoubanAuthManager()
var body: some Scene {
WindowGroup {
ContentView()
// 通过环境注入共享实例
.environment(authManager)
.environment(DoubanManager(authManager: authManager))
}
}
}
SwiftUI 中的依赖注入
Swift 中有多重依赖注入的实现方式,例如环境注入、构造函数注入、属性注入和组合对象模式等。
其中最常用的是环境注入和构造函数注入。
构造函数注入(又名初始化器注入)
构造函数注入(Constructor Injection)就是在初始化器(init)中注入依赖,因此又叫做初始化器注入(Initializer Injection)。
典型的实现方式如下:
@Observable
class DoubanManager {
// authManager 作为依赖
private let authManager: DoubanAuthManager
// 构造函数注入
init(authManager: DoubanAuthManager) {
self.authManager = authManager
}
// 其他方法可以使用这个依赖
}
// 使用时必须提供依赖
let authManager = DoubanAuthManager()
let doubanManager = DoubanManager(authManager: authManager)
这种方式就是构造函数注入(Constructor Injection),是依赖注入的最常用形式。
环境注入(Environment Injection)
@Observable
class DoubanManager {
func fetchUserFavoriteMovies(authManager: DoubanAuthManager) async -> [DoubanMovie] {
guard authManager.isLoggedIn else { return [] }
// ... 其他代码
}
}
// 在视图中使用
struct MovieListView: View {
@Environment(DoubanManager.self) private var doubanManager
@Environment(DoubanAuthManager.self) private var authManager
var body: some View {
List {
// 调用时传入 authManager
await doubanManager.fetchUserFavoriteMovies(authManager: authManager)
}
}
}
如何选择?
构造函数注入和环境注入两种方式都很常用,适用于不同的场景。例如,
- 构造函数注入:适合服务类之间的依赖(如 DoubanManager 依赖 DoubanAuthManager)。
- 环境注入:适用于视图层访问服务。
依赖注入的几种初始化方式
在 SwiftUI 应用中,通常会在应用入口处创建这些实例并通过环境传递。
错误方式一
避免使用下面这种方式,这是一种错误的方式:
@main
struct FilmoApp: App {
// 先创建 authManager
@State private var authManager = DoubanAuthManager()
// 使用 authManager 创建 doubanManager
@State private var doubanManager = DoubanManager(authManager: DoubanAuthManager())
var body: some Scene {
WindowGroup {
ContentView()
.environment(authManager)
.environment(doubanManager)
}
}
}
这里的重大问题是:创建了两个不同的 DoubanAuthManager 实例。
// 先创建 authManager
@State private var authManager = DoubanAuthManager()
// 使用 authManager 创建 doubanManager - 但实际上没有使用上面的实例!
@State private var doubanManager = DoubanManager(authManager: DoubanAuthManager())
- 第一行创建了一个 authManager 实例
- 第二行又创建了一个全新的 DoubanAuthManager 实例作为参数传入。
错误方式二
另一种常见的错误方式:
@main
struct FilmoApp: App {
// 先创建 authManager
@State private var authManager = DoubanAuthManager()
// 延迟初始化 doubanManager,使用已有的 authManager
@State private var doubanManager: DoubanManager
init() {
// 正确引用上面创建的 authManager
_doubanManager = State(initialValue: DoubanManager(authManager: authManager))
}
var body: some Scene {
WindowGroup {
ContentView()
.environment(authManager)
.environment(doubanManager)
}
}
}
在初始化器中,Xcode 会提示:
'self' used before all stored properties are initialized
这个错误是因为在初始化过程中,尝试访问还未完全初始化的 authManager 属性。
在 Swift 中,使用 @State 修饰的属性实际上是一个属性包装器 (Property Wrapper),其完整初始化是在结构体所有其他存储属性初始化之后才完成的。
正确方式一:使用临时变量(推荐)
@main
struct FilmoApp: App {
@State private var authManager: DoubanAuthManager
@State private var doubanManager: DoubanManager
init() {
// 1. 首先创建临时的认证管理器
let tempAuthManager = DoubanAuthManager()
// 2. 使用临时变量初始化 State 属性
_authManager = State(initialValue: tempAuthManager)
// 3. 使用同一个临时变量创建 doubanManager
_doubanManager = State(initialValue: DoubanManager(authManager: tempAuthManager))
// 现在所有属性都已初始化,可以安全访问 self
}
var body: some Scene {
WindowGroup {
ContentView()
.environment(authManager)
.environment(doubanManager)
}
}
}
正确方式二:使用服务容器
服务容器模式的好处是,不用创建中间变量。
2025 年 3 月 17 日记录:这种方式虽然可以正常工作,但是似乎不符合 Swift 语言的风格,也没有在 Apple 的示例代码中看到类似的实现。
因此,我决定先暂时不在我的项目中使用这种设计模式。
常见疑问
如何访问或更新被依赖项?
可以直接通过环境访问 authManager (推荐)
在使用依赖注入的架构中,通常 DoubanAuthManager
和 DoubanManager
会同时通过环境注入到视图中,因此,被依赖项也是一个单独的实例,我们可以直接访问它:
struct LoginView: View {
// 直接访问认证管理器
@Environment(DoubanAuthManager.self) private var authManager
var body: some View {
Button("登录") {
// 直接更新认证状态
Task {
await performLogin()
// 登录成功后直接更新状态
authManager.isLoggedIn = true
authManager.userName = "获取到的用户名"
authManager.saveLoginStatus()
}
}
}
}
这种方式符合职责分离原则,登录相关操作直接与认证管理器交互。