Swift如何最小化依赖外部库实现依赖注入并注入ViewController
你有Kotlin DI经验的话,理解Swift的DI逻辑会非常快,核心思路完全一致:不要在依赖使用方内部创建依赖,由外部创建好依赖之后传入。
你看到的博客示例省略了最核心的入口上下文,而且本身写法存在缺陷:VC内部给requestManager设置了默认的RequestManager()实例,这会导致外部注入变成可选操作——哪怕你忘记赋值,VC依然会用内部创建的实例,既无法在单元测试时替换Mock依赖,也可能出现依赖实例不统一的问题。
1. 零依赖原生实现(优先推荐)
完全不需要引入任何第三方库,Swift语言本身的特性就足够支撑DI实现。
1.1 属性注入(你看到的示例对应的完整实现)
首先修正示例里的VC代码,移除内部的默认依赖初始化:
import UIKit class ViewController: UIViewController { // 移除内部默认实例化逻辑,依赖完全由外部传入 // 用隐式解包是因为VC初始化时不会立刻加载视图,只要在viewDidLoad调用前注入就不会崩溃 var requestManager: RequestManager! }
VC实例的创建、依赖注入的位置,完全取决于页面的启动/跳转入口,最常见的有3种场景:
- App启动根页面场景:在
SceneDelegate(iOS13+)或AppDelegate(旧版本系统)的根页面配置逻辑里创建// iOS13+ SceneDelegate 根页面配置代码 func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { guard let windowScene = (scene as? UIWindowScene) else { return } let window = UIWindow(windowScene: windowScene) // 1. 创建VC实例 let viewController = ViewController() // 2. 注入依赖 viewController.requestManager = RequestManager() // 3. 配置为根页面 window.rootViewController = viewController self.window = window window.makeKeyAndVisible() } - 代码跳转(push/present)场景:在触发跳转的上一个页面里,跳转前创建VC并注入
// 上一个页面中的跳转逻辑 @objc func tapNextButton() { // 1. 创建目标VC let targetVC = ViewController() // 2. 注入依赖,可直接传入当前页面持有的同实例,实现全局依赖复用 targetVC.requestManager = self.requestManager // 3. 执行跳转 navigationController?.pushViewController(targetVC, animated: true) } - Storyboard/Xib加载场景:在调用
instantiateViewController方法拿到VC实例之后,跳转之前注入
如果用Storyboard的自动Segue跳转,要在let storyboard = UIStoryboard(name: "Main", bundle: nil) let targetVC = storyboard.instantiateViewController(withIdentifier: "ViewController") as! ViewController // 注入依赖 targetVC.requestManager = RequestManager() // 执行跳转 present(targetVC, animated: true)prepare(for:sender:)方法里拿到目标VC完成注入:override func prepare(for segue: UIStoryboardSegue, sender: Any?) { guard let targetVC = segue.destination as? ViewController else { return } targetVC.requestManager = RequestManager() }
属性注入的优点是写法简单,兼容Storyboard/Xib;缺点是依赖是隐式要求的,编译器无法检查是否漏注入,一旦忘记赋值就会触发运行时崩溃。
1.2 构造器注入(类型安全,纯代码页面首选)
和Kotlin里通过主构造器传依赖的逻辑完全一致,Swift支持给VC自定义构造方法,编译器会强制要求传入所有必填依赖,从根源上避免漏注入的问题:
import UIKit class ViewController: UIViewController { // 用let声明,依赖注入后不可修改 let requestManager: RequestManager // 自定义构造器,要求外部传入必填依赖 init(requestManager: RequestManager) { self.requestManager = requestManager super.init(nibName: nil, bundle: nil) } // 兼容Storyboard/Xib的必需构造方法,不允许直接调用 required init?(coder: NSCoder) { fatalError("请使用 init(requestManager:) 方法初始化ViewController") } }
这种写法下创建VC时必须传入依赖,漏写参数会直接编译报错,根本没有机会触发运行时错误:
// 必须传requestManager才能创建实例 let viewController = ViewController(requestManager: RequestManager()) // 直接配置跳转即可,不需要额外赋值属性 window.rootViewController = viewController
如果项目是纯代码开发UI,构造器注入是零依赖方案里最稳妥的选择。
1.3 轻量工厂封装
如果项目规模较大、依赖层级较多,可以简单实现一个页面工厂类,统一管理所有VC的创建和依赖注入逻辑,避免每个跳转处都重复初始化依赖:
class ViewControllerFactory { static let shared = ViewControllerFactory() // 全局单例依赖在这里统一初始化 private let globalRequestManager = RequestManager() func makeHomePage() -> ViewController { ViewController(requestManager: globalRequestManager) } // 其他页面的创建方法... }
使用时直接调用工厂方法即可,不需要关心页面依赖什么参数:
let homeVC = ViewControllerFactory.shared.makeHomePage() navigationController?.pushViewController(homeVC, animated: true)
2. 成熟第三方DI方案
如果想要类似Koin/Hilt的容器能力,不想自己维护工厂逻辑,目前Swift生态里最推荐的轻量、维护稳定的库是Swinject:
- 无代码生成,接入成本极低,和Koin的使用逻辑几乎一致
- 支持单例、工厂等多种依赖生命周期,支持自动解析依赖树
- 兼容UIKit和SwiftUI
简单用法示例:
import Swinject // 全局容器 let container = Container() // 注册依赖 container.register(RequestManager.self) { _ in RequestManager() } container.register(ViewController.self) { resolver in ViewController(requestManager: resolver.resolve(RequestManager.self)!) } // 解析获取VC实例,依赖自动注入 let homeVC = container.resolve(ViewController.self)!
不推荐使用需要大量代码生成、侵入性强的DI库,后续Swift版本升级时维护成本极高。
常见踩坑提醒
- 不要在VC内部直接调用
RequestManager()初始化依赖,否则DI完全失去解耦意义 - 不要全局滥用单例
RequestManager.shared硬编码在VC逻辑里,会导致测试时无法替换Mock - 不要在viewDidLoad里才初始化依赖,会导致依赖和VC生命周期强绑定,无法灵活替换
内容的提问来源于stack exchange,提问作者Dylon Jaynes

