依赖注入是一种相当流行的模式,它使您可以灵活地配置系统并正确建立此系统组件之间的依赖关系。 通过键入,Swift允许您使用方便的框架,通过它们可以非常简短地描述依赖关系图。 今天,我想谈一谈这些框架之一DITranquillity
。
本教程将介绍以下库功能:
- 类型注册
- 初始化部署
- 嵌入变量
- 组件循环依赖
- 将库与
UIStoryboard
一起UIStoryboard
组件说明
该应用程序将包含以下主要组件: ViewController
, Router
, Presenter
, Networking
这些是任何iOS应用程序中非常常见的组件。
ViewController
和Router
将循环引入。
准备工作
首先,在Xcode中创建一个Single View应用程序,使用CocoaPods添加DITranquillity 。 创建必要的文件层次结构,然后向Main.storyboard添加第二个控制器,并使用StoryboardSegue
连接。 结果,应获得以下文件结构:
在类中创建依赖项,如下所示:
组件声明 protocol Presenter: class { func getCounter(completion: @escaping (Int) -> Void) } class MyPresenter: Presenter { private let networking: Networking init(networking: Networking) { self.networking = networking } func getCounter(completion: @escaping (Int) -> Void) {
protocol Networking: class { func fetchData(completion: @escaping (Result<Int, Error>) -> Void) } class MyNetworking: Networking { func fetchData(completion: @escaping (Result<Int, Error>) -> Void) {
protocol Router: class { func presentNewController() } class MyRouter: Router { unowned let viewController: ViewController init(viewController: ViewController) { self.viewController = viewController } func presentNewController() {
class ViewController: UIViewController { var presenter: Presenter! var router: Router! }
局限性
与其他类不同, ViewController
不是由我们创建的,而是由UIStoryboard.instantiateViewController
实现中的UIKit库UIStoryboard.instantiateViewController
,因此,使用情节UIStoryboard.instantiateViewController
,我们无法使用初始化程序将依赖项注入UIViewController
的继承人中。 UIView
和UITableViewCell
的继承人也是如此。
请注意,隐藏在协议后面的对象嵌入在所有类中。 这是实现依赖关系的主要任务之一-使依赖关系不依赖于实现,而依赖于接口。 将来,这将有助于为重用或测试组件提供不同的协议实现。
依赖注入
创建系统的所有组件之后,我们继续进行对象之间的连接。 在DITranquillity中,起点是DIContainer
,它使用container.register(...)
方法添加注册。 要将依赖关系分为多个部分,必须DIPart
DIFramework
和DIPart
。 为了方便起见,我们将仅创建一个ApplicationDependency
类,该类将实现DIFramework
并将用作所有依赖项的注册位置。 DIFramework
接口仅要求您实现一种方法DIFramework
load(container:)
。
class ApplicationDependency: DIFramework { static func load(container: DIContainer) {
让我们从没有依赖性的最简单的注册开始MyNetworking
container.register(MyNetworking.init)
该注册使用通过初始化程序的实现。 尽管组件本身没有依赖关系,但必须提供初始化程序以使库清楚地知道如何创建组件。
同样,注册MyPresenter
和MyRouter
。
container.register1(MyPresenter.init) container.register1(MyRouter.init)
注意:请注意,不是register
使用,而是register1
。 不幸的是,这是必要的,以指示对象在初始化程序中是否只有一个依赖项。 也就是说,如果存在0个或两个或多个依赖关系,则只需要使用register
。 此限制是Swift 4.0及更高版本的错误。
现在该注册我们的ViewController
。 它不是通过初始化程序而是直接将对象注入变量,因此注册描述会更多。
container.register(ViewController.self) .injection(cycle: true, \.router) .injection(\.presenter)
\.presenter
格式的语法为SwiftKeyPath,因此可以简洁地实现依赖关系。 由于Router
和ViewController
周期性地相互依赖,因此必须使用cycle: true
明确指出这一点。 库本身可以解决这些依赖关系而无需明确指示,但是引入了此要求,以便阅读该图的人员可以立即了解到依赖关系链中存在循环。 还要注意, 不是使用 ViewController.init
, 而是使用 ViewController.self
。 上面在“ 限制”部分中对此进行了描述。
还必须使用特殊方法注册UIStoryboard
。
container.registerStoryboard(name: "Main")
现在,我们已经描述了一个屏幕的整个依赖图。 但是,尚无访问此图的权限。 您必须创建一个DIContainer
,以允许您访问其中的对象。
static let container: DIContainer = { let container = DIContainer()
- 初始化容器
- 向其添加图形描述。
- 我们检查是否做对了所有事情。 如果出错,则应用程序将不会在解决依赖关系期间崩溃,而是在创建图形时立即崩溃
然后,您需要使容器成为应用程序的起点。 为此, AppDelegate
在didFinishLaunchingWithOptions
实现didFinishLaunchingWithOptions
方法,而不是在项目设置Main.storyboard
指定为启动点。
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { window = UIWindow(frame: UIScreen.main.bounds) let storyboard: UIStoryboard = ApplicationDependency.container.resolve() window?.rootViewController = storyboard.instantiateInitialViewController() window?.makeKeyAndVisible() return true }
发射
第一次启动时,将发生丢弃,并且由于以下原因,验证将失败:
- 容器将找不到类型
Router
, Presenter
, Networking
,因为我们仅注册了对象。 如果我们不希望访问实现,而是访问接口,则必须显式指定接口 - 容器不了解如何解决循环依赖性,因为有必要明确指出每次图解析时都不应重新创建哪些对象。
解决第一个错误很简单-有一个特殊的方法可以让您指定容器中的方法在哪些协议下可用。
container.register(MyNetworking.init) .as(check: Networking.self) {$0}
如下描述注册,我们说:可通过Networking
协议访问MyNetworking
对象。 必须对协议下隐藏的所有对象执行此操作。 {$0}
添加用于编译器正确检查类型。
第二个错误要复杂一些。 必须使用所谓的scope
,该scope
描述了对象创建的频率和寿命。 对于每个参与循环依赖的注册,您必须指定一个等于objectGraph
的scope
。 这将使容器清楚地知道,在解析过程中,有必要重用相同的已创建对象,而不是每次都创建。 因此,结果是:
container.register(ViewController.self) .injection(cycle: true, \.router) .injection(\.presenter) .lifetime(.objectGraph) container.register1(MyRouter.init) .as(check: Router.self) {$0} .lifetime(.objectGraph)
重新启动后,容器成功通过验证,并且我们的ViewController随创建的依赖关系一起打开。 您可以在viewDidLoad
放置一个断点并确定。
屏幕之间的过渡
接下来,创建两个小类SecondViewController
和SecondPresenter
,将SecondViewController
添加到情节SecondViewController
中,并在它们之间创建一个标识符为"RouteToSecond"
的Segue
,使您可以从第一个打开第二个控制器。
为每个新类向我们的ApplicationDependency
添加两个注册:
container.register(SecondViewController.self) .injection(\.secondPresenter) container.register(SecondPresenter.init)
无需指定.as
,因为我们没有将SecondPresenter
隐藏在协议后面,而是直接使用实现。 然后,在第一个控制器的viewDidAppear
方法中,我们调用performSegue(withIdentifier: "RouteToSecond", sender: self)
,启动,打开第二个控制器,应在其中附加secondPresenter
依赖项。 如您所见,容器看到了从UIStoryboard
创建的第二个控制器,并成功放下了依赖项。
结论
该库使您可以方便地使用循环依赖项,情节提要,并充分利用Swift中的自动类型推断,从而提供了一种非常简短而灵活的语法来描述依赖关系图。
参考文献
github库中的完整示例代码
github上的 DITranquillity
英文文章