Cleanse 是一个为 Swift 设计的 依赖注入 框架。它的设计从一开始就以开发者体验为中心。它的灵感来源于 Dagger 和 Guice。
亲爱的 Cleanse 社区,
我们在此宣布,Cleanse 仓库将于 2024 年 6 月 13 日正式弃用。 多年来,Cleanse 一直致力于为 Swift 依赖注入提供强大的解决方案,我们非常感谢社区的支持和贡献。
此决定基于 Square 决定使用不同的依赖注入框架。我们相信这一步将使我们能够专注于其他项目和创新,从而更好地满足用户和更广泛的开源生态系统的需求。
我们鼓励您 fork 这个仓库,或探索以下可能更好地满足您需求的替代方案
如果您有兴趣维护此仓库,或有任何其他疑问,请通过 opensource+external@squareup.com 联系我们。
我们衷心感谢每一位为 Cleanse 做出贡献的人,无论是通过代码、文档还是社区支持。您的努力是无价的,我们深表感谢。感谢您成为 Cleanse 旅程的一部分。
衷心感谢,Square 的 Cleanse 团队
这是一个关于如何在您的应用程序中开始使用 Cleanse 的快速指南。
在 Examples/CleanseGithubBrowser
中可以找到一个使用 Cleanse 和 Cocoa Touch 的完整示例
您可以使用以下命令将最新的 Cleanse 版本拉取到您的 Podfile
中
pod 'Cleanse'
可以将 Cleanse.xcodeproj
拖放到 Xcode 中现有的项目或工作区中。可以将 Cleanse.framework
添加为目标依赖项并嵌入它。
Cleanse 应该能够通过 Carthage 进行配置。应该能够按照 向应用程序添加 Frameworks 中的说明,从 Carthage 的 README 中成功完成此操作。
Cleanse 可以与 Swift Package Manager 一起使用。以下是可以添加到 Project
声明的依赖项中的定义。Xcode 11 中添加 Cleanse 作为包依赖项受 v4.2.5 及更高版本支持。
特性 | Cleanse 实现状态 |
---|---|
多重绑定 | 支持 (.intoCollection() ) |
覆盖 | 支持 |
Objective-C 兼容层 | 支持 |
属性注入 [1] | 支持 |
类型限定符 | 通过 类型标签 支持 |
辅助注入 | 支持 |
子组件 | 通过 组件 支持 |
服务提供者接口 | 支持 |
cleansec (Cleanse 编译器) | 实验性 |
[1] | 属性注入在其他 DI 框架中被称为 字段注入 |
DI 框架的另一个非常重要的部分是它如何处理错误。快速失败是理想的。Cleanse 的设计旨在支持快速失败。它目前支持一些常见错误的快速失败,但尚未完成
错误类型 | Cleanse 实现状态 |
---|---|
缺少提供者 | 支持 [2] |
重复绑定 | 支持 |
循环检测 | 支持 |
[2] | 当缺少提供者时,错误会显示提供者需要的行号等信息。Cleanse 还会在失败前收集所有错误 |
Cleanse API 位于名为 Cleanse
的 Swift 模块中(令人惊讶吗?)。要在文件中使用其任何 API,必须在顶部导入它。
import Cleanse
Cleanse 负责构建一个图(或更具体地说是一个 有向无环图),它表示您的所有依赖项。此图以根对象开始,根对象连接到其直接依赖项,而这些依赖项又持有到其依赖项的边,依此类推,直到我们获得应用程序对象图的完整图景。
使用 Cleanse 管理依赖项的入口点是从定义一个“根”对象开始的,该对象在构造时返回给您。在 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>)
。这些函数非常重要,因为它们将包含关于我们如何在应用程序中构造每个对象/依赖项的逻辑。参数和返回类型现在令人困惑,但随着我们的深入,它们会变得更有意义。
第一个函数是任何 Component 都需要的,因为它告诉 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<E> 的实例。否则,子组件可能会意外地被释放。
// 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
注入到我们的 RootViewContoller
中并设置背景颜色。
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
添加更多依赖项,并添加更多 模块 来满足它们。
可能值得查看我们的 示例应用程序,以查看更完整功能的示例。
包装其包含类型的值。提供与 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
指示标签对其有效的类型。这与 Java 中用作 Dagger 和 Guice 中的限定符的注解非常不同,后者无法通过它们应用的类型来约束。
在 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 中的模块与 Dagger 或 Guice 等其他 DI 系统中的模块用途相似。模块是对象图的构建块。在 Cleanse 中使用模块可能与熟悉 Guice 的人非常相似,因为配置是在运行时完成的,并且绑定 DSL 非常受 Guice 的启发。
Module
协议只有一个方法 configure(binder:)
,其定义如下
protocol Module {
func configure<B : Binder>(binder: B)
}
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/")!)
}
}
注意:通常,最好将配置 X 的 Module
嵌入为 X 的名为 Module
的内部结构体。为了消除 Cleanse 的 Module 协议与正在定义的内部结构体之间的歧义,必须使用 Cleanse.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)
}
}
}
Cleanse 具有 Component
的概念。Component
表示依赖项的对象图,该对象图在构造时返回 Root
关联类型,并用作 Cleanse 的“入口点”。但是,我们也可以使用 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)
,Cleanse 将在您的对象图中自动创建 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(())
}
}
辅助注入用于组合种子参数和预绑定依赖项。与子组件具有用于构建对象图的 Seed
类似,辅助注入允许您通过创建具有定义的 Seed
对象的 Factory
类型,以便通过 build(_:)
函数进行构造,从而消除样板代码。
假设我们有一个详细信息视图控制器,它根据用户从列表视图控制器中的选择来显示特定客户的信息。
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)
}
}
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 有两个核心方法,通常会与它们交互。第一个也是更简单的一个是 install 方法。将要安装的模块的实例传递给它。它的用法如下
binder.include(module: PrimaryAPIURLModule.self)
它本质上告诉 binder 在 PrimaryAPIURLModule
上调用 configure(binder:)
。
Binder 公开的另一个核心方法是 bind<E>(type: E.Type)
。这是配置绑定的入口点。bind 方法接受一个参数,即正在配置的元素的 元类型。bind()
返回一个 BindingBuilder
,必须在其上调用方法才能完成已启动的绑定的配置。
bind()
和后续的 builder 方法(不是终止方法)都用 @warn_unused_result
注解,以防止由于仅部分配置绑定而导致的错误。
bind()
的 type
参数具有默认值,可以在某些常见情况下推断和省略。在本文档中,我们有时会显式指定它以提高可读性。
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
都需要自己的 Scope 类型。因此,如果将其配置为 RootComponent 中的单例,则将为整个应用程序返回相同的实例。
Cleanse 为您提供了两个作用域:Unscoped
和 Singleton
。Unscoped
是默认作用域,它始终构造一个新对象,而 Singleton
是为了方便而提供的,但不是必须使用的。它最常用于应用程序的 RootComponent
的作用域类型。
要完成绑定的配置,必须调用 BindingBuilder
上的终止方法之一。有多种方法被认为是终止步骤。下面描述了常见的步骤。
这是一类终止方法,用于配置如何实例化不依赖于对象图中配置的其他实例的元素。
其他终止方法都汇集到此方法。如果 Element
的绑定以此变体终止,则当请求 Element
的实例时,将在 provider 参数上调用 .get()
。
这是一个便捷方法。它在语义上等同于 .to(provider: Provider(value: value))
或 .to(factory: { value })
。它将来可能会提供性能优势,但目前没有。
这需要一个闭包而不是 provider,但在其他方面是等效的。等同于 .to(provider: Provider(getter: factory))
这就是我们定义绑定需求的方式。Dagger 2 通过查看 @Provides
方法和 @Inject
构造函数的参数在编译时确定需求。Guice 也做了类似的事情,但使用反射来确定参数。可以通过 getProvider() 方法从 Guice 的 binder 显式请求依赖项。
与 Java 不同,Swift 没有注解处理器在编译时执行此操作,也没有稳定的反射 API。我们也不想公开类似 getProvider() 的方法,因为它允许用户执行危险的操作,并且还会丢失关于哪些 provider 依赖于其他 provider 的重要信息。
但是,Swift 确实有一个非常强大的泛型系统。我们利用这一点在创建绑定时提供安全性和简洁性。
这将 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)
}
由于依赖 provider 的请求发生在配置时,因此对象图在配置时知道所有绑定和依赖项,并将快速失败。
好吧,我们可能需要多个需求才能构造给定的实例。Swift 中没有 可变参数泛型。但是,我们使用了一个小脚本来生成各种阶数的 to(factory:)
方法。
有时希望将同一类型的多个对象提供到一个集合中。一个非常常见的用法是将拦截器或过滤器提供给 RPC 库。在应用程序中,可能希望添加到标签栏控制器的视图控制器集合中,或设置页面中的设置。
提供给 Set 或 Dictionary 不是不需要的功能,并且可能会构建为在提供给 Arrays
之上的扩展。
将元素绑定到集合与标准 绑定步骤 非常相似,但增加了一个步骤:在 builder 定义中调用 .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:)}
此 builder 序列的 终止步骤 可以是单个 Element
或 Element
数组的工厂/值/provider。
在某些情况下,用户不控制对象的构造,但依赖注入被认为是很有用的。其中一些更常见的情况是
Cleanse 为此提供了一个解决方案:属性注入(在 Guice 和 Dagger 中称为成员注入)。
在 Cleanse 中,属性注入在设计上是二等公民。工厂/构造函数注入应尽可能使用,但在不可能的情况下可以使用属性注入。属性注入具有类似于 BindingBuilder
的 builder 语言
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 中的属性注入器不知道类层次结构。如果用户希望属性注入在类层次结构中级联,则绑定的注入器可以为父类调用 inject 方法,或者请求 PropertyInjector<Superclass>
作为注入器参数并使用它。
我们可以通过 属性注入 使 App Delegate 成为 Cleanse 对象图的根。我们必须在此处使用属性注入,因为我们不控制 app delegate 的构造。现在,我们可以将我们的“根”建模为 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)。这是一个简短的表格,涵盖了我们的基础知识,并确保您有资格做出贡献。