Cleanse - Swift 依赖注入
Cleanse 是一个为 Swift设计的 依赖注入 框架。它从头开始设计,以 开发者体验 为前提,并从 Dagger 和 Guice 中汲取灵感。
入门指南
这是一份快速入门指南,介绍如何在您的应用中使用 Cleanse。
有关在 Cocoa Touch 中使用 Cleanse 的完整示例,请参阅 Examples/CleanseGithubBrowser
安装
使用 CocoaPods
您可以使用以下命令将最新版本的 Cleanse 拖入到您的 Podfile
中
pod 'Cleanse'
使用 Xcode
Cleanse.xcodeproj
可以拖放到 Xcode 中现有的项目或工作区中。可以将 Cleanse.framework
添加为目标依赖项并将其嵌入。
Carthage
使用Cleanse 可以使用 Carthage 进行配置。应遵循Carthage 的 README中的将框架添加到应用部分以成功完成此操作。
Swift Package Manager
使用Cleanse 可以与 Swift Package Manager 一起使用。以下是一个可以添加到 Project
声明的依赖项定义。从 Xcode 11 开始,支持将 Cleanse 作为包依赖项添加,版本 4.2.5 及以上。
特性
特性 | Cleanse 实现状态 |
---|---|
多绑定 | 支持(.intoCollection() ) |
覆盖 | 支持 |
Objective-C 兼容层 | 支持 |
属性注入 [1] | 支持 |
类型限定符 | 通过 类型标签 支持 |
辅助注入 | 支持 |
子组件 | 通过 组件 支持 |
服务提供器接口 | 支持 |
cleansec(Cleanse 编译器) | 实验性 |
[1] | 属性注入在其他的 DI 框架中称为 字段注入 |
DI 框架的另一个非常重要的部分是它如何处理错误。尽早失败是理想的情况。Cleanse 被设计成支持快速失败。它目前对一些更常见的错误支持快速失败,但还不是完全的。
错误类型 | Cleanse 实现状态 |
---|---|
缺失提供者 | 支持 [2] |
重复绑定 | 支持 |
循环检测 | 支持 |
[2] | 当供应商缺失时,错误会出现在需要供应商的行号等位置。Cleanse 还会在失败前收集所有错误。 |
使用 Cleanse
Cleanse API 在一个名为 Cleanse
的 Swift 模块中(令人惊讶?)。要在文件中使用它的任何 API,必须在顶部导入它。
import Cleanse
定义组件和根类型
Cleanse 负责构建一个图(或更具体地说,是一个 有向无环图),该图表示您所有的依赖项。这个图以一个根对象开始,它与它的直接依赖项相连,这些依赖项持有到其依赖项的边,以此类推,直到我们有应用程序对象图的完整视图。
使用 Cleanse 管理您的依赖项的起点是定义一个“Root”对象,它在构建时返回给您。在 Cocoa Touch 应用程序中,我们的根对象可以是我们在应用程序的 UIWindow
上设置的 rootViewController
对象。(更逻辑上讲,根对象是 App Delegate,但我们无法控制其构建,因此我们需要使用属性注入。您可以在 高级设置指南 中了解更多信息)
让我们首先定义 RootComponent
。
struct Component : Cleanse.RootComponent {
// When we call build(()) it will return the Root type, which is a RootViewController instance.
typealias Root = RootViewController
// Required function from Cleanse.RootComponent protocol.
static func configureRoot(binder bind: ReceiptBinder<RootViewController>) -> BindingReceipt<RootViewController> {
}
// Required function from Cleanse.RootComponent protocol.
static func configure(binder: Binder<Unscoped>) {
// We will fill out contents later.
}
}
创建我们的根组件后,我们会发现需要实现两个函数:static func configureRoot(binder bind: ReceiptBinder<RootViewController>) -> BindingReceipt<RootViewController>
和 static func configure(binder: Binder<Unscoped>)
。这些函数非常重要,因为它们将包含构建我们应用程序中每个对象/依赖项的逻辑。参数和返回类型现在看起来很混乱,但在我们继续前进时会有意义。
第一个函数是任何组件都必须实现的,因为它告诉 Cleanse 如何构建根对象。让我们填写内容以配置我们的 RootViewController
的构建方式。
static func configureRoot(binder bind: ReceiptBinder<RootViewController>) -> BindingReceipt<RootViewController> {
return bind.to(factory: RootViewController.init)
}
现在,让我们创建我们的 RootViewController
类。
class RootViewController: UIViewController {
init() {
super.init(nibName: nil, bundle: nil)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .blue
}
}
我们已经成功地连接了我们的根组件!我们的根对象 RootViewController
配置得当,因此我们可以在 App Delegate 中构建组件(和图形)以使用它。
重要:您必须保留由 ComponentFactory.of(:) 返回的 ComponentFactory
// IMPORTANT: We must retain an instance of our `ComponentFactory`.
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var factory: ComponentFactory<AppDelegate.Component>?
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// Build our root object in our graph.
factory = try! ComponentFactory.of(AppDelegate.Component.self)
let rootViewController = factory!.build(())
// Now we can use the root object in our app.
window!.rootViewController = rootViewController
window!.makeKeyAndVisible()
return true
}
满足依赖项
运行应用现在将显示我们的根视图控制器RootViewController
,带有蓝色背景。然而,这并不太有趣也不太现实,因为我们的RootViewController
很可能需要许多依赖来设置应用。所以,让我们创建一个简单的依赖项RootViewProperties
,它将持有我们根视图的背景色(以及其他未来的属性)。
struct RootViewProperties {
let backgroundColor: UIColor
}
然后将RootViewProperties
注入到我们的RootViewController
并设置背景色。
class RootViewController: UIViewController {
let rootViewProperties: RootViewProperties
init(rootViewProperties: RootViewProperties) {
self.rootViewProperties = rootViewProperties
super.init(nibName: nil, bundle: nil)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = rootViewProperties.backgroundColor
}
}
现在运行应用将导致一个新的错误,表示缺少RootViewProperties
的提供者。这是因为我们从RootViewController
类中引用了它,但Cleanse没有找到与RootViewProperties
类型相关的绑定。所以让我们创建一个!我们将在之前讨论过我们的根组件内部的static func configure(binder: Binder<Unscoped>)
函数中这样做。
static func configure(binder: Binder<Unscoped>) {
binder
.bind(RootViewProperties.self)
.to { () -> RootViewProperties in
RootViewProperties(backgroundColor: .blue)
}
}
现在我们已经满足了RootViewProperties
的依赖关系,我们应该能够成功启动并看到之前相同的蓝色背景。
随着应用功能的增长,人们可以在RootViewController
中添加更多的依赖项,以及更多的模块来满足它们。
查看我们的示例应用,以了解更全面的示例可能值得。
核心概念 & 数据类型
Provider
/ProviderProtocol
包装其包含类型的值。具有与Java的javax.inject.Provider相同的功能。
Provider
和TaggedProvider
(见下文)实现了ProviderProtocol
协议,该协议定义如下:
public protocol ProviderProtocol {
associatedtype Element
func get() -> Element
}
类型标签
在给定组件中,可能有提供或要求不同意义上的通用类型不同实例的需求。例如,我们可能需要区分API服务器的基本URL和我们的临时目录的URL。
在Java中,这使用注解完成,特别是带有@Qualifier的注解。在Go中,这可以通过字段的标签来完成。
在Cleanse的系统架构中,类型注解等于Tag协议的实现。
public protocol Tag {
associatedtype Element
}
关联类型Element
指示标签可以应用于何类型。这与Dagger和Guice在Java中用作限定符的注解完全不同,这些注解无法约束它们应用于的类型。
在Cleanse中,实现了Tag
协议来区分类型,并使用TaggedProvider
封装Tag.Element
类型的值。由于大多数库都引用了ProviderProtocol
,所以几乎在需要Provider
的地方都可以使用TaggedProvider
。
其定义几乎与Provider
相同,只是在额外的一个泛型参数。
struct TaggedProvider<Tag : Cleanse.Tag> : ProviderProtocol {
func get() -> Tag.Element
}
示例
假设我们想标注一个URL类型,比如API端点的基础URL,可以这样定义标签
public struct PrimaryAPIURL : Tag {
typealias Element = NSURL
}
然后,可以通过使用类型来请求这个特殊URL的TaggedProvider
。
TaggedProvider<PrimaryAPIURL>
如果我们有一个需要此URL来执行函数的类,构造函数可以这样定义
class SomethingThatDoesAnAPICall {
let primaryURL: NSURL
init(primaryURL: TaggedProvider<PrimaryAPIURL>) {
self.primaryURL = primaryURL.get()
}
}
模块
Cleanse中的模块在其他DI系统(如Dagger或Guice)中充当类似的作用。模块是对象图的基本构建块。在使用Cleanse中的模块方面,对于那些熟悉Guice的人来说可能看起来非常相似,因为配置在运行时进行,而绑定DSL受到了Guice的很大启发。
Module
协议只有一个方法,即configure(binder:)
,其定义如下
protocol Module {
func configure<B : Binder>(binder: B)
}
示例
提供基础API URL
struct PrimaryAPIURLModule : Module {
func configure(binder: Binder<Unscoped>) {
binder
.bind(NSURL.self)
.tagged(with: PrimaryAPIURL.self)
.to(value: NSURL(string: "https://connect.squareup.com/v2/")!)
}
}
https://connect.squareup.com/v2/”)
消耗主API URL(例如,“注意:通常将配置X的Module
内嵌到X的名为Module
的内联结构中是一种良好的实践。为了区分Clearse的Module
协议与正在定义的内联结构,需要用Clearse.Module
来限定协议。
class SomethingThatDoesAnAPICall {
let primaryURL: NSURL
init(primaryURL: TaggedProvider<PrimaryAPIURL>) {
self.primaryURL = primaryURL.get()
}
struct Module : Cleanse.Module {
func configure(binder: Binder<Unscoped>) {
binder
.bind(SomethingThatDoesAnAPICall.self)
.to(factory: SomethingThatDoesAnAPICall.init)
}
}
}
组件
Clearse有一个Component
的概念。Component
代表我们在构建时的依赖关系对象图,并在构造时返回关联的类型Root
,它是进入Clearse的“入口点”。然而,我们也可以用Component
在父级对象图中创建子图,这被称为子组件。子组件与作用域密切相关,并用来确定依赖项的作用域。组件内的对象只能注入在同一组件(或作用域)或祖先组件中存在的依赖项。使用组件进行依赖项作用域的一个示例是,通过让LoggedInComponent
继承应用程序的根组件。这使得你可以在LoggedInComponent
中绑定特定的登录对象,如会话令牌或账户对象,以防止意外将依赖项泄漏到欢迎流程视图中使用的对象。
基本组件协议定义为
public protocol ComponentBase {
/// This is the binding required to construct a new Component. Think of it as somewhat of an initialization value.
associatedtype Seed = Void
/// This should be set to the root type of object that is created.
associatedtype Root
associatedtype Scope: Cleanse._ScopeBase = Unscoped
static func configure(binder: Binder<Self.Scope>)
static func configureRoot(binder bind: ReceiptBinder<Root>) -> BindingReceipt<Root>
}
对象图最外层的组件(例如,根组件),是由ComponentFactory的build(())
方法构建的。这被定义为以下协议扩展
public extension Component {
/// Builds the component and returns the root object.
public func build() throws -> Self.Root
}
示例
定义子组件
struct RootAPI {
let somethingUsingTheAPI: SomethingThatDoesAnAPICall
}
struct APIComponent : Component {
typealias Root = RootAPI
func configure(binder: Binder<Unscoped>) {
// "include" the modules that create the component
binder.include(module: PrimaryAPIURLModule.self)
binder.include(module: SomethingThatDoesAnAPICall.Module.self)
// bind our root Object
binder
.bind(RootAPI.self)
.to(factory: RootAPI.init)
}
}
使用组件
通过调用binder.install(dependency: APIComponent.self)
,Clearse将在你的对象图中自动创建类型的ComponentFactory<APIComponent>
。
struct Root : RootComponent {
func configure(binder: Binder<Unscoped>) {
binder.install(dependency: APIComponent.self)
}
// ...
}
然后,你可以通过将ComponentFactory<APIComponent>
实例注入到对象中并调用build(())
来使用它。
class RootViewController: UIViewController {
let loggedInComponent: ComponentFactory<APIComponent>
init(loggedInComponent: ComponentFactory<APIComponent>) {
self.loggedInComponent = loggedInComponent
super.init(nibName: nil, bundle: nil)
}
func logIn() {
let apiRoot = loggedInComponent.build(())
}
}
辅助注入
RFC #112)
摘要 (在合并种子参数和预绑定依赖项时使用辅助注入。与子组件有用于构建对象图的Seed
类似,辅助注入允许您通过创建一个定义了用于通过build(_:)
函数进行构造的Seed
对象的Factory
类型来消除样板代码。
示例
创建工厂
假设我们有一个详细视图控制器,它根据从列表视图控制器中用户的选中项显示特定客户的详细信息。
class CustomerDetailViewController: UIViewController {
let customerID: String
let customerService: CustomerService
init(customerID: Assisted<String>, customerService: CustomerService) {
self.customerID = customerID.get()
self.customerService = customerService
}
...
}
在我们的初始化器中,我们有一个Assisted<String>
,它表示基于从列表视图控制器选中的客户ID的辅助注入参数,以及一个预绑定的依赖项CustomerService
。
为了创建我们的工厂,我们需要定义一个符合AssistedFactory
的类型来设置我们的Seed
和Element
类型。
extension CustomerDetailViewController {
struct Seed: AssistedFactory {
typealias Seed = String
typealias Element = CustomerDetailViewController
}
}
创建我们的AssistedFactory
对象后,我们可以通过Cleanse创建工厂绑定。
extension CustomerDetailViewController {
struct Module: Cleanse.Module {
static func configure(binder: Binder<Unscoped>) {
binder
.bindFactory(CustomerDetailViewController.self)
.with(AssistedFactory.self)
.to(factory: CustomerDetailViewController.init)
}
}
}
消耗我们的工厂
创建绑定后,Cleanse会将一个Factory<CustomerDetailViewController.AssistedFactory>
类型绑定到我们的对象图中。因此,在我们的客户列表视图控制器中消耗此工厂可能看起来像这样
class CustomerListViewController: UIViewController {
let detailViewControllerFactory: Factory<CustomerDetailViewController.AssistedFactory>
init(detailViewControllerFactory: Factory<CustomerDetailViewController.AssistedFactory>) {
self.detailViewControllerFactory = detailViewControllerFactory
}
...
func tappedCustomer(with customerID: String) {
let detailVC = detailViewControllerFactory.build(customerID)
self.present(detailVC, animated: false)
}
}
服务提供者界面
摘要(《RFC #118》链接)
Cleanse提供了开发者连接生成对象图以创建自定义验证和工具的插件界面。
创建插件可以是3个步骤完成的。
1. 创建您的插件实现,以符合协议 CleanseBindingPlugin
您需要实现函数 func visit(root: ComponentBinding, errorReporter: CleanseErrorReporter)
,该函数为您提供ComponentBinding
和CleanseErrorReporter
的一个实例。
第一个参数ComponentBinding
表示根组件,可以用来遍历整个对象图。第二个CleanseErrorReporter
用于在验证完成后向用户报告错误。
2. 使用 CleanseServiceLoader
实例注册您的插件
创建一个CleanseServiceLoader
实例后,您可以通过register(_:)
函数注册您的插件。
3. 将您的服务加载器传递给 RootComponent
工厂函数
工厂函数RootComponent
,public static func of(_:validate:serviceLoader:)
接受一个CleanseServiceLoader
实例,并将运行该对象中注册的所有插件。
注意:只有当您在工厂函数中将validate设置为true时,您的插件才会运行。
可以在上述链接的RFC中找到示例插件实现。
绑定器
将Binder
实例传递给Module.configure(binder:)
,模块实现使用它来配置他们的提供者。
绑定的两个核心方法通常用于接口。第一个也是比较简单的一个是安装方法。您将一个模块的实例传递给它。用法如下
binder.include(module: PrimaryAPIURLModule.self)
它实际上是指示绑定器调用configure(binder:)
在PrimaryAPIURLModule
上。
绑定器公开的另一个核心方法是bind
。这是配置绑定的人口。bind方法接受一个参数,即配置元素的类型。bind()返回一个BindingBuilder
,必须调用它来完成初始化的绑定的配置。
带有bind()
以及后续的非终止性builder方法的bind()
都带有@warn_unused_result
注解,以防止仅部分配置绑定时出错。
在 bind()
函数中,type
参数有一个默认值,在一些常见情况下可以推断并省略。在本文档中,我们有时会明确指定它以提高可读性。
BindingBuilder
及配置您的绑定
BindingBuilder 是一个流畅的 API,用于配置您的绑定。它以引导用户通过代码补全进行绑定配置的方式构建。BindingBuilder 的 DSL 语法简化为
binder .bind([Element.self]) // Bind Step [.tagged(with: Tag_For_Element.self)] // Tag step [.sharedInScope()] // Scope step {.to(provider:) | // Terminating step .to(factory:) | .to(value:)}
绑定步骤
这开始绑定过程,定义如何创建 Element
的实例
标签步骤(可选)
这是一个可选步骤,表示提供的类型应该是 TaggedProvider<Element>
,而不是仅仅 Provider<Element>
。
详见:类型标记
范围步骤
默认情况下,每次请求对象时,Cleanse 都会构造一个新的对象。如果指定了可选的 .sharedInScope(),Cleanse 将会在 Component
的作用域中缓存并返回相同的实例。Component
需要自己的作用域类型。因此,如果它在 RootComponent 中配置为单例,那么它将为整个应用程序返回相同的实例。
Cleanse 为您提供了两个作用域:Unscoped
和 Singleton
。Unscoped
是默认作用域,将始终构造一个新的对象;而 Singleton
仅出于方便而提供,并非必须使用。它最常用作应用程序 RootComponent
的作用域类型。
终止步骤
为了完成配置绑定,必须调用 BindingBuilder
上的某个终止方法。存在多个被认为是终止步骤的方法。下面描述的是常见的几个。
无依赖终止方法
这是一类终止方法,用于配置如何实例化不受对象图中其它实例配置依赖的元素。
to(provider: Provider<E>)
终止方法:其他终止方法都会流入这个。如果使用这个变体终止 Element
的绑定,当请求 Element
的实例时,将会在提供者参数上调用 .get()
。
to(value: E)
终止方法:这是一个便捷方法。它在语义上等同于 .to(provider: Provider(value: value))
或 .to(factory: { value })
。在将来可能提供性能优势,但目前并不提供。
to(factory: () -> E)
(0参数)
终止方法:它接受一个闭包而不是提供者,但其他方面等效。等同于 .to(provider: Provider(getter: factory))
依赖请求终止方法
我们就是这样定义绑定的要求的。Dagger 2通过检查@Provides
方法和@Inject
构造函数的参数,在编译时确定要求。Guice也做类似的事情,但使用反射来确定参数。可以通过Guice的getProvider()方法显式请求依赖。
与Java不同,Swift没有用于编译时代码处理的注解处理器,也没有稳定的反射API。我们也不希望公开类似getProvider()的方法,因为它允许人们进行危险的操作,并且让人们丢失重要的信息,即哪些提供者依赖于其他提供者。
然而,Swift有一个非常强大的泛型系统。我们利用这个系统在创建绑定时提供安全性和简单性。
to<P1>(factory: (P1) -> E)
(第1价)
终止方法:这注册了一个从工厂函数到E的绑定,这个工厂函数接受一个参数。
它的工作原理
假设我们定义了一个汉堡,如下所示
struct Hamburger {
let topping: Topping
// Note: this actually would be created implicitly for structs
init(topping: Topping) {
self.topping = topping
}
}
当不调用初始化器(例如let factory = Hamburger.init
)而引用它时,该表达式结果是一个函数类型为
(topping: Topping) -> Hamburger
所以,当在一个模块中配置其创建时,调用
binder.bind(Hamburger.self).to(factory: Hamburger.init)
将调用.to<P1>(factory: (P1) -> E)
终止函数,并将Element
解析为Hamburger
,将P1
解析为Topping
。
此to(factory:)
的伪实现
public func to<P1>(factory: (P1) -> Element) {
// Ask the binder for a provider of P1. This provider
// is invalid until the component is constructed
// Note that getProvider is an internal method, unlike in Guice.
// It also specifies which binding this provider is for to
// improve debugging.
let dependencyProvider1: Provider<P1> =
binder.getProvider(P1.self, requiredFor: Element.self)
// Create a Provider of Element. This will call the factory
// method with the providers
let elementProvider: Provider<Element> = Provider {
factory(dependencyProvider1.get())
}
// Call the to(provider:) terminating function to finish
// this binding
to(provider: elementProvider)
}
由于依赖提供者是在配置时间请求的,对象图在配置时间就知道所有绑定和依赖,并且会快速失败。
to<P1, P2, … PN>(factory: (P1, P2, … PN) -> E)
(第N价)
终止方法:好吧,我们可能需要多于一个要求来构建特定的实例。Swift中没有变长泛型。然而,我们使用了一个小脚本来生成各种价的to(factory:)
方法。
集合绑定
有时我们希望将多个相同类型的对象添加到一个集合中。这一个非常常见的用途是为RPC库提供拦截器或过滤器。在一个应用中,我们可能希望向标签栏控制器的视图控制器集合或设置页面的设置中添加。
这个概念被称为 多绑定,在Dagger中(参见Dagger的多绑定)和在Guice中(参见Guice的多绑定)。
向集合或字典中提供并不是一个不希望的功能,并且可能会作为在提供到Arrays
之上的一个扩展来构建。
将一个元素绑定到一个集合中的操作与标准的绑定步骤非常相似,但增加了一个步骤:在构建定义中调用.intoCollection()
。
binder .bind([Element.self]) // Bind Step .intoCollection() // indicates that we are providing an // element or elements into Array<Element>** [.tagged(with: Tag_For_Element.self)] // Tag step [.asSingleton()] // Scope step {.to(provider:) | // Terminating step .to(factory:) | .to(value:)}
这个构建序列的终止步骤可以是单个Element
或Element
的Array
的工厂/值/提供者。
属性注入
有一些情况下,我们不能控制对象的建设,但注射依赖是有用的。一些更常见的这种出现包括
- App Delegate:每个iOS应用都需要它,它是入口点,但UIKit将构建它。
- 通过Storyboard构建的视图控制器(特别是通过segues):是的,我们都犯过错误。其中一个可能是在Storyboard变得难以控制之前使用Storyboard。当使用Storyboard时,我们没有控制视图控制器的建设。
- XCTestCase:我们不控制它们的实例化方式,但可能需要从对象图中访问对象。这在更高级别的测试中更受欢迎,如UI和集成测试(通常可以在较低级别的单元测试中避免DI)
Cleansere对此有一个解决方案:属性注入(在Guice和Dagger中被称为成员注入)。
在cleansere中,属性注入按设计是二等公民。应该优先使用工厂/构造函数注入,但在不能时,可以使用属性注入。属性注入有类似BindingBuilder
的构建语言
binder
.bindPropertyInjectionOf(<metatype of class being injected into>)
.to(injector: <property injection method>)
终止函数有两种变体,一种是不带参数的签名。
(Element, P1, P2, ..., Pn) -> ()
另一种是
(Element) -> (P1, P2, ..., Pn) -> ()
前一个是为了允许使用简单的注入方法,这些方法不是实例方法,例如
binder
.bindPropertyInjectionOf(AClass.self)
.to {
$0.a = ($1 as TaggedProvider<ATag>).get()
}
或
binder
.bindPropertyInjectionOf(BClass.self)
.to {
$0.injectProperties(superInjector: $1, b: $2, crazyStruct: $3)
}
可用于后者类型的注入方法(Element -> (P1, P2, ..., Pn) -> ()
)当引用目标上的实例方法时很方便。
假设我们有一个
class FreeBeer {
var string1: String!
var string2: String!
func injectProperties(
string1: TaggedProvider<String1>,
string2: TaggedProvider<String2>
) {
self.string1 = string1.get()
self.string2 = string2.get()
}
}
可以通过以下方式绑定FreeBeer的属性注入
binder
.bindPropertyInjectionOf(FreeBeer.self)
.to(injector: FreeBeer.injectProperties)
FreeBeer.injectProperties
表达式的结果类型为FreeBeer -> (TaggedProvider<String1>, TaggedProvider<String2>) -> ()
绑定了对 Element
的属性注入器后,在工厂参数中能够请求 PropertyInjector<Element>
类型。它定义了一个单独的方法,如下:
func injectProperties(into instance: Element)
然后它将执行对 Element
的属性注入。
**注意**:非遗留API中的属性注入器不知道类层次结构。如果想让属性注入在类层次结构中叠加,注入器可以调用 super 的 inject 方法,或者请求作为注入器参数的 PropertyInjector<Superclass>
并使用它。
高级设置
通过 属性注入,我们可以使我们的 Cleanse 对象图的根为 App Delegate。我们必须在这里使用属性注入,因为我们无法控制 app delegate 的构造。现在我们可以将我们的 "Root" 模型为一个 PropertyInjector<AppDelegate>
类型的实例,然后使用该对象将属性注入到已经构造好的 App Delegate 中。
让我们首先重新定义 RootComponent
。
extension AppDelegate {
struct Component : Cleanse.RootComponent {
// When we call build() it will return the Root type, which is a PropertyInjector<AppDelegate>.
// More on how we use the PropertyInjector type later.
typealias Root = PropertyInjector<AppDelegate>
// Required function from Cleanse.RootComponent protocol.
static func configureRoot(binder bind: ReceiptBinder<PropertyInjector<AppDelegate>>) -> BindingReceipt<PropertyInjector<AppDelegate>> {
return bind.propertyInjector(configuredWith: { bind in
bind.to(injector: AppDelegate.injectProperties)
})
}
// Required function from Cleanse.RootComponent protocol.
static func configure(binder: Binder<Unscoped>) {
// Binding go here.
}
}
}
在我们的 app delegate 中,增加 injectProperties
函数。
func injectProperties(_ window: UIWindow) {
self.window = window
}
现在要将新的根对象连接起来,我们可以在 app delegate 中调用 injectProperties(:)
。
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// Build our component, and make the property injector
let propertyInjector = try! ComponentFactory.of(AppDelegate.Component.self).build(())
// Now inject the properties into ourselves
propertyInjector.injectProperties(into: self)
window!.makeKeyAndVisible()
return true
}
现在运行应用会产生一个新的错误,说明缺少 UIWindow
的提供者,但绑定我们的 UIWindow
实例及其依赖项后,我们就可以继续运行了!
extension UIWindow {
struct Module : Cleanse.Module {
public func configure(binder: Binder<Singleton>) {
binder
.bind(UIWindow.self)
// The root app window should only be constructed once.
.sharedInScope()
.to { (rootViewController: RootViewController) in
let window = UIWindow(frame: UIScreen.mainScreen().bounds)
window.rootViewController = rootViewController
return window
}
}
}
}
贡献
我们很高兴你对 Cleanse 感兴趣,并且我们很乐意看到你如何将其用于实践。
向 master Cleanse 仓库的任何贡献者都必须签署 个人贡献者许可协议 (CLA)。这是一个短表单,涵盖了我们的基础并确保你有资格做出贡献。