• Stars
    star
    1,773
  • Rank 25,197 (Top 0.6 %)
  • Language
    Swift
  • License
    Other
  • Created almost 8 years ago
  • Updated about 1 year ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Lightweight Swift Dependency Injection Framework

Cleanse - Swift Dependency Injection

image

image

image

Cleanse is a dependency injection framework for Swift. It is designed from the ground-up with developer experience in mind. It takes inspiration from both Dagger and Guice.

Getting Started

This is a quick guide on how to get started using Cleanse in your application.

A full-fledged example of using Cleanse with Cocoa Touch can be found in Examples/CleanseGithubBrowser

Installation

Using CocoaPods

You can pull in the latest Cleanse version into your Podfile using

pod 'Cleanse'

Using Xcode

Cleanse.xcodeproj can be dragged and dropped into an existing project or workspace in Xcode. One may add Cleanse.framework as a target dependency and embed it.

Using Carthage

Cleanse should be able to be configured with Carthage. One should be able to follow the Adding Frameworks to an Application from Carthage's README to successfully do this.

Cleanse can be used with Swift Package Manager. The following a definition that can be added to the dependencies of a Project declaration. Adding Cleanse as a package dependency in Xcode 11 is supported by v4.2.5 and above.

Features

Feature Cleanse Implementation Status
Multi-Bindings Supported (.intoCollection())
Overrides Supported
Objective-C Compatibility layer Supported
Property Injection1 Supported
Type Qualifiers Supported via Type Tags
Assisted Injection Supported
Subcomponents Supported via Components
Service Provider Interface Supported
cleansec (Cleanse Compiler) Experimental

Another very important part of a DI framework is how it handles errors. Failing fast is ideal. Cleanse is designed to support fast failure. It currently supports fast failing for some of the more common errors, but it isn't complete

Error Type Cleanse Implementation Status
Missing Providers Supported2
Duplicate Bindings Supported
Cycle Detection Supported

Using Cleanse

The Cleanse API is in a Swift module called Cleanse (surprised?). To use any of its API in a file, at the top, one must import it.

import Cleanse

Defining a Component and Root Type

Cleanse is responsible for building a graph (or more specifically a directed acyclic graph) that represents all of your dependencies. This graph starts with a root object which is connected to its immediate dependencies, and those dependencies hold edges to its dependencies and so on until we have a complete picture of your application's object graph.

The entry point into managing your dependencies with Cleanse starts by defining a "Root" object that is returned to you upon construction. In a Cocoa Touch application, our root object could be the rootViewController object we set on the application's UIWindow. (More logically the root object is the App Delegate, however since we don't control construction of that we would have to use Property Injection. You can read more about this in the Advanced Setup guide)

Let's begin by defining the 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.
    }
}

After creating our root component, we find that we're required to implement two functions: static func configureRoot(binder bind: ReceiptBinder<RootViewController>) -> BindingReceipt<RootViewController> and static func configure(binder: Binder<Unscoped>). These functions are very important because they will contain the logic for how we construct every object/dependency in our app. The parameters and return types are confusing right now, but will make more sense as we go along.

The first function is required of any Component since it tells Cleanse how to construct the root object. Let's fill in the contents to configure how we will construct our RootViewController.

static func configureRoot(binder bind: ReceiptBinder<RootViewController>) -> BindingReceipt<RootViewController> {
    return bind.to(factory: RootViewController.init)
}

Now, let's create our RootViewController class

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
    }
}

We've successfully wired up our root component! Our root object RootViewController is configured properly, so in our App Delegate we can now build the component (and graph) to use it.

Important: It is important that you retain an instance of the ComponentFactory<E> returned from ComponentFactory.of(:). Otherwise subcomponents may unexpectedly become deallocated.

// 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
    }

Satisfying Dependencies

Running the app will now display our RootViewController with a blue background. However this is not very interesting nor realistic as our RootViewController will likely require many dependencies to set up our app. So let's create a simple dependency RootViewProperties that will hold the background color of our root view (among other future properties).

struct RootViewProperties {
    let backgroundColor: UIColor
}

And then inject RootViewProperties into our RootViewContoller and set the background color.

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
    }
}

Running the app now will yield a new error saying a provider for RootViewProperties is missing. That's because we referenced it from our RootViewController class, but Cleanse didn't find a binding for the RootViewProperties type. So let's create one! We will do this inside the static func configure(binder: Binder<Unscoped>) function we talked about earlier inside our root component.

static func configure(binder: Binder<Unscoped>) {
      binder
          .bind(RootViewProperties.self)
          .to { () -> RootViewProperties in
              RootViewProperties(backgroundColor: .blue)
          }
  }

Now that we have satisfied the RootViewProperties dependency, we should be able to successfully launch and see the same blue background as before.

As the functionality of this app grows, one may add more dependencies to RootViewController as well as more Modules to satisfy them.

It may be worth taking a look at our example app to see a more full-featured example.

Core Concepts & Data Types

Provider/ProviderProtocol

Wraps a value of its containing type. Serves the same functionality as Java's javax.inject.Provider.

Provider and TaggedProvider (see below) implement ProviderProtocol protocol which is defined as:

public protocol ProviderProtocol {
    associatedtype Element
    func get() -> Element
}

Type Tags

In a given component, there may be the desire to provide or require different instances of common types with different significances. Perhaps we need to distinguish the base URL of our API server from the URL of our temp directory.

In Java, this is done with annotations, in particular ones annotated with @Qualifier. In Go, this can be accomplished with tags on structs of fields.

In Cleanse's system a type annotation is equivalent to an implementation of the Tag protocol:

public protocol Tag {
    associatedtype Element
}

The associatedtype, Element, indicates what type the tag is valid to apply to. This is very different than annotations in Java used as qualifiers in Dagger and Guice which cannot be constrained by which type they apply to.

In Cleanse, the Tag protocol is implemented to distinguish a type, and the TaggedProvider is used to wrap a value of Tag.Element. Since most of the library refers to ProviderProtocol, TaggedProvider is accepted almost everywhere a Provider is.

Its definition is almost identical to Provider aside from an additional generic argument:

struct TaggedProvider<Tag : Cleanse.Tag> : ProviderProtocol {
    func get() -> Tag.Element
}

Example

Say one wanted to indicate a URL type, perhaps the base URL for the API endpoints, one could define a tag this way:

public struct PrimaryAPIURL : Tag {
    typealias Element = NSURL
}

Then one may be able to request a TaggedProvider of this special URL by using the type:

TaggedProvider<PrimaryAPIURL>

If we had a class that requires this URL to perform a function, the constructor could be defined like:

class SomethingThatDoesAnAPICall {
    let primaryURL: NSURL
    init(primaryURL: TaggedProvider<PrimaryAPIURL>) {
        self.primaryURL = primaryURL.get()
    }
}

Modules

Modules in Cleanse serve a similar purpose to Modules in other DI systems such as Dagger or Guice. Modules are building blocks for one's object graph. Using modules in Cleanse may look very similar to those familiar with Guice since configuration is done at runtime and the binding DSL is very inspired by Guice's.

The Module protocol has a single method, configure(binder:), and is is defined as:

protocol Module {
    func configure<B : Binder>(binder: B)
}

Examples

Providing the Base 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/")!)
  }
}
Consuming the Primary API URL (e.g. "https://connect.squareup.com/v2/")

Note: It is generally a good practice to embed the Module that configures X as an inner struct of X named Module. To disambiguate Cleanse's Module protocol from the inner struct being defined, one has to qualify the protocol with 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)
        }
    }
}

Components

Cleanse has a concept of a Component. A Component represents an object graph of our dependencies that returns the Root associated type upon construction and is used as the "entry point" into Cleanse. However, we can also use a Component to create a subgraph inside our parent object graph, called a subcomponent. Subcomponents are closely related to scopes and are used to scope your dependencies. Objects inside a component are only allowed to inject dependencies that exist within the same component (or scope), or an ancestor's component. A parent component is not allowed to reach into a subcomponent and retrieve a dependency. One example of using components to scope dependencies is by having a LoggedInComponent inherting from your application's Root component. This allows you to bind logged in specific objects such as session tokens or account objects within the LoggedInComponent so that you can't accidently leak these dependencies into objects used outside of a logged session (i.e welcome flow views).

The base component protocol is defined as:

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>
}

The outermost component of an object graph (e.g. the Root component), is built by the build(()) method on ComponentFactory. This is defined as the following protocol extension:

public extension Component {
    /// Builds the component and returns the root object.
    public func build() throws -> Self.Root
}

Examples

Defining a subcomponent
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)
    }
}
Using the component

Cleanse will automatically create the type ComponentFactory<APIComponent> in your object graph by calling binder.install(dependency: APIComponent.self).

struct Root : RootComponent {
    func configure(binder: Binder<Unscoped>) {
        binder.install(dependency: APIComponent.self)
    }
    // ...
}

And then you can use it by injecting in the ComponentFactory<APIComponent> instance into an object and calling 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(())
    }
}

Assisted Injection

Summary (RFC #112)

Assisted injection is used when combining seeded parameters and pre-bound dependencies. Similar to how a subcomponent has a Seed that is used to build the object graph, assisted injection allows you to eliminate boilerplate by creating a Factory type with a defined Seed object for construction via the build(_:) function.

Examples

Creating a factory

Say we have a detail view controller that displays a particular customer's information based on the user's selection from a list view controller.

class CustomerDetailViewController: UIViewController {
    let customerID: String
    let customerService: CustomerService
    init(customerID: Assisted<String>, customerService: CustomerService) {
        self.customerID = customerID.get()
        self.customerService = customerService
    }
    ...
}

In our initializer, we have Assisted<String> which represents an assisted injection parameter based on the customer ID selected from the list view controller, and a pre-bound dependency CustomerService.

In order to create our factory, we need to define a type that conforms to AssistedFactory to set our Seed and Element types.

extension CustomerDetailViewController {
    struct Seed: AssistedFactory {
        typealias Seed = String
        typealias Element = CustomerDetailViewController
    }
}

Once we create our AssistedFactory object, we can create the factory binding through Cleanse.

extension CustomerDetailViewController {
    struct Module: Cleanse.Module {
        static func configure(binder: Binder<Unscoped>) {
            binder
              .bindFactory(CustomerDetailViewController.self)
              .with(AssistedFactory.self)
              .to(factory: CustomerDetailViewController.init)
        }
    }
}
Consuming our factory

After creating our binding, Cleanse will bind a Factory<CustomerDetailViewController.AssistedFactory> type into our object graph. So in our customer list view controller, consuming this factory may look like:

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)
    }
}

Service Provider Interface

Summary (RFC #118)

Cleanse provides a plugin interface that developers can use to hook into the generated object graph to create custom validations and tooling.

Creating a plugin can be done in 3 steps:

1. Create your plugin implementation by conforming to the protocol CleanseBindingPlugin

You will be required to implement the function func visit(root: ComponentBinding, errorReporter: CleanseErrorReporter), which hands you an instance of a ComponentBinding and CleanseErrorReporter.

The first parameter, ComponentBinding, is a representation of the root component and can be used to traverse the entire object graph. The second, CleanseErrorReporter is used to report errors back to the user after validation is complete.

2. Register your plugin with a CleanseServiceLoader instance

After creating an instance of a CleanseServiceLoader, you can register your plugin via the register(_:) function.

3. Pass your service loader into the RootComponent factory function

The RootComponent factory function, public static func of(_:validate:serviceLoader:) accepts a CleanseServiceLoader instance and will run all the plugins registered within that object.

NOTE: Your plugins will only be run if you set validate to true in the factory function.

Sample plugin implementations are available in the RFC linked above.

Binder

A Binder instance is what is passed to Module.configure(binder:) which module implementations use to configure their providers.

Binders have two core methods that one will generally interface with. The first, and simpler one, is the install method. One passes it an instance of a module to be installed. It is used like:

binder.include(module: PrimaryAPIURLModule.self)

It essentially tells the binder to call configure(binder:) on PrimaryAPIURLModule.

The other core method that binders expose is the bind<E>(type: E.Type). This is the entry point to configure a binding. The bind methods takes one argument, which the metattype of the element being configured. bind() returns a BindingBuilder that one must call methods on to complete the configuration of the binding that was initiated.

bind() and subsequent builder methods that are not terminating are annotated with @warn_unused_result to prevent errors by only partially configuring a binding.

The type argument of bind() has a default and can be inferred and omitted in some common cases. In this documentation we sometimes specify it explicitly to improve readability.

BindingBuilder and Configuring Your Bindings

The BindingBuilder is a fluent API for configuring your bindings. It is built in a way that guides one through the process of configuring a binding through code completion. A simplified grammar for the DSL of BindingBuilder is:

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:)}

Bind Step

This starts the binding process to define how an instance of Element is created

Tag Step (Optional)

An optional step that indicates that the provided type should actually be TaggedProvider<Element> and not just Provider<Element>.

See: Type Tags for more information

Scope Step

By default, whenever an object is requested, Cleanse constructs a new one. If the optional .sharedInScope() is specified, Cleanse will memoize and return the same instance in the scope of the Component it was configured in. Each Component requires its own Scope type. So if this is configured as a singleton in the RootComponent, then will return the same instance for the entire app.

Cleanse provides two scopes for you: Unscoped and Singleton. Unscoped is the default scope that will always construct a new object, and Singleton is provided out of convenience but not necessary to use. It is most commonly used as the scope type for your application's RootComponent.

Terminating Step

To finish configuring a binding, one must invoke one of the terminating methods on BindingBuilder. There are multiple methods that are considered terminating steps. The common ones are described below.

Dependency-Free Terminating methods

This is a category of terminating methods that configure how to instantiate elements that don't have dependencies on other instances configured in the object graph.

Terminating Method: to(provider: Provider<E>)

Other terminating methods funnel into this. If the binding of Element is terminated with this variant, .get() will be invoked on the on the provider argument when an instance of Element is requested.

Terminating Method: to(value: E)

This is a convenience method. It is semantically equivalent to .to(provider: Provider(value: value)) or .to(factory: { value }). It may offer performance advantages in the future, but currently doesn't.

Terminating Method: to(factory: () -> E) (0th arity)

This takes a closure instead of a provider, but is otherwise equivalent. Is equivalent to .to(provider: Provider(getter: factory))

Dependency-Requesting Terminating Methods

This is how we define requirements for bindings. Dagger 2 determines requirements at compile time by looking at the arguments of @Provides methods and @Inject constructors. Guice does something similar, but using reflection to determine arguments. One can explicitly request a dependency from Guice's binder via the getProvider() method.

Unlike Java, Swift doesn't have annotation processors to do this at compile time, nor does it have a stable reflection API. We also don't want to expose a getProvider()-like method since it allows one to do dangerous things and also one loses important information on which providers depend on other providers.

Swift does, however, have a very powerful generic system. We leverage this to provide safety and simplicity when creating our bindings.

Terminating Methods: to<P1>(factory: (P1) -> E) (1st arity)

This registers a binding of E to the factory function which takes one argument.

How it works

Say we have a hamburger defined as:

struct Hamburger {
   let topping: Topping
   // Note: this actually would be created implicitly for structs
   init(topping: Topping) {
     self.topping = topping
   }
 }

When one references the initializer without calling it (e.g. let factory = Hamburger.init), the expression results in a function type of

(topping: Topping) -> Hamburger

So when configuring its creation in a module, calling

binder.bind(Hamburger.self).to(factory: Hamburger.init)

will result in calling the .to<P1>(factory: (P1) -> E) terminating function and resolve Element to Hamburger and P1 to Topping.

A pseudo-implementation of this 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)
}

Since the requesting of the dependent providers happen at configuration time, the object graph is aware of all the bindings and dependencies at configuration time and will fail fast.

Terminating Methods: to<P1, P2, … PN>(factory: (P1, P2, … PN) -> E) (Nth arity)

Well, we may have more than one requirement to construct a given instance. There aren't variadic generics in swift. However we used a small script to generate various arities of the to(factory:) methods.

Collection Bindings

It is sometimes desirable to provide multiple objects of the same type into one collection. A very common use of this would be providing interceptors or filters to an RPC library. In an app, one may want to add to a set of view controllers of a tab bar controller, or settings in a settings page.

This concept is referred to as Multibindings in Dagger and in Guice.

Providing to a Set or Dictionary is not an unwanted feature and could probably be built as an extension on top of providing to Arrays.

Binding an element to a collection is very similar to standard Bind Steps, but with the addition of one step: calling .intoCollection() in the builder definition.:

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:)}

The Terminating Step for this builder sequence can either be a factory/value/provider of a single Element or Array of Elements.

There are a few instances where one does not control the construction of an object, but dependency injection would be deemed useful. Some of the more common occurrences of this are: - App Delegate: This is required in every iOS app and is the entry point, but UIKit will construct it. - View Controllers constructed via storyboard (in particular via segues): Yes, we all make mistakes. One of those mistakes may have been using Storyboards before they became unwieldy. One does not control the construction of view controllers when using storyboards. - XCTestCase: We don't control how they're instantiated, but may want to access objects from an object graph. This is more desirable in higher levels of testing such as UI and integration testing (DI can usually be avoided for lower level unit tests) Cleanse has a solution for this: Property injection (known as Member injection in Guice and Dagger). In cleanse, Property injection is a second class citizen by design. Factory/Constructor injection should be used wherever possible, but when it won't property injection may be used. Property Injection has a builder language, similar to theBindingBuilder: .. code-block:: swift binder .bindPropertyInjectionOf(<metatype of class being injected into>) .to(injector: <property injection method>) There are two variants of the terminating function, one is where the signature is .. code-block:: swift (Element, P1, P2, ..., Pn) -> () And the other is .. code-block:: swift (Element) -> (P1, P2, ..., Pn) -> () The former is to allow for simple injection methods that aren't instance methods, for example: .. code-block:: swift binder .bindPropertyInjectionOf(AClass.self) .to { $0.a = ($1 as TaggedProvider<ATag>).get() } or .. code-block:: swift binder .bindPropertyInjectionOf(BClass.self) .to { $0.injectProperties(superInjector: $1, b: $2, crazyStruct: $3) } The latter type of injection method that can be used (Element -> (P1, P2, …, Pn) -> ()) is convenient when referring to instant methods on the target for injection. Say we have .. code-block:: swift class FreeBeer { var string1: String! var string2: String! func injectProperties( string1: TaggedProvider<String1>, string2: TaggedProvider<String2> ) { self.string1 = string1.get() self.string2 = string2.get() } } One can bind a property injection for FreeBeer by doing: .. code-block:: swift binder .bindPropertyInjectionOf(FreeBeer.self) .to(injector: FreeBeer.injectProperties) The result type of the expressionFreeBeer.injectPropertiesisFreeBeer -> (TaggedProvider<String1>, TaggedProvider<String2>) -> ()After binding a property injector forElement, one will be able to request the typePropertyInjector<Element>in a factory argument. This has a single method defined as: .. code-block:: swift func injectProperties(into instance: Element) Which will then perform property injection intoElement. **Note:** Property injectors in the non-legacy API are unaware of class hierarchies. If one wants property injection to cascade up a class hierarchy, the injector bound may call the inject method for super, or request aPropertyInjector<Superclass>as an injector argument and use that. We can make the root of our Cleanse object graph the App Delegate through Property Injection. We must use property injection here because we don't control construction of the app delegate. Now we can model our "Root" as an instance of PropertyInjector<AppDelegate> and then use this object to inject properties into our already constructed App Delegate.

Let's start by redefining the 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.
    }
  }
}

Inside of our app delegate, we add the function injectProperties:

func injectProperties(_ window: UIWindow) {
  self.window = window
}

Now to wire up our new root object, we can call injectProperties(:) on ourself in the app delegate:

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
}

Running the app now will yield a new error saying a provider for UIWindow is missing, but after binding an instance of our UIWindow and its dependencies, we should be good to go!

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
        }
    }
  }
}

Contributing

We're glad you're interested in Cleanse, and we'd love to see where you take it.

Any contributors to the master Cleanse repository must sign the Individual Contributor License Agreement (CLA). It's a short form that covers our bases and makes sure you're eligible to contribute.

License

Apache 2.0


  1. Property injection is known as field injection in other DI frameworks↩

  2. When a provider is missing, errors present line numbers, etc. where the provider was required. Cleanse will also collect all errors before failing↩

More Repositories

1

okhttp

Square’s meticulous HTTP client for the JVM, Android, and GraalVM.
Kotlin
45,250
star
2

retrofit

A type-safe HTTP client for Android and the JVM
HTML
42,581
star
3

leakcanary

A memory leak detection library for Android.
Kotlin
29,130
star
4

picasso

A powerful image downloading and caching library for Android
Kotlin
18,656
star
5

javapoet

A Java API for generating .java source files.
Java
10,691
star
6

moshi

A modern JSON library for Kotlin and Java.
Kotlin
9,502
star
7

okio

A modern I/O library for Android, Java, and Kotlin Multiplatform.
Kotlin
8,667
star
8

dagger

A fast dependency injector for Android and Java.
Java
7,317
star
9

crossfilter

Fast n-dimensional filtering and grouping of records.
JavaScript
6,222
star
10

PonyDebugger

Remote network and data debugging for your native iOS app using Chrome Developer Tools
Objective-C
5,867
star
11

maximum-awesome

Config files for vim and tmux.
Ruby
5,706
star
12

otto

An enhanced Guava-based event bus with emphasis on Android support.
Java
5,174
star
13

cubism

Cubism.js: A JavaScript library for time series visualization.
JavaScript
4,930
star
14

sqlbrite

A lightweight wrapper around SQLiteOpenHelper which introduces reactive stream semantics to SQL operations.
Java
4,574
star
15

android-times-square

Standalone Android widget for picking a single date from a calendar view.
Java
4,437
star
16

wire

gRPC and protocol buffers for Android, Kotlin, Swift and Java.
Kotlin
4,172
star
17

Valet

Valet lets you securely store data in the iOS, tvOS, or macOS Keychain without knowing a thing about how the Keychain works. It’s easy. We promise.
Swift
3,957
star
18

cube

Cube: A system for time series visualization.
JavaScript
3,912
star
19

kotlinpoet

A Kotlin API for generating .kt source files.
Kotlin
3,805
star
20

java-code-styles

IntelliJ IDEA code style settings for Square's Java and Android projects.
Shell
2,957
star
21

flow

Name UI states, navigate between them, remember where you've been.
Java
2,789
star
22

spoon

Distributing instrumentation tests to all your Androids.
HTML
2,700
star
23

keywhiz

A system for distributing and managing secrets
Java
2,614
star
24

tape

A lightning fast, transactional, file-based FIFO for Android and Java.
Java
2,459
star
25

certstrap

Tools to bootstrap CAs, certificate requests, and signed certificates.
Go
2,194
star
26

mortar

A simple library that makes it easy to pair thin views with dedicated controllers, isolated from most of the vagaries of the Activity life cycle.
Java
2,159
star
27

go-jose

An implementation of JOSE standards (JWE, JWS, JWT) in Go
1,981
star
28

assertj-android

A set of AssertJ helpers geared toward testing Android.
Java
1,578
star
29

haha

DEPRECATED Java library to automate the analysis of Android heap dumps.
Java
1,436
star
30

phrase

Phrase is an Android string resource templating library
Java
1,404
star
31

cane

Code quality threshold checking as part of your build
Ruby
1,325
star
32

seismic

Android device shake detection.
Java
1,275
star
33

anvil

A Kotlin compiler plugin to make dependency injection with Dagger 2 easier.
Kotlin
1,265
star
34

sudo_pair

Plugin for sudo that requires another human to approve and monitor privileged sudo sessions
Rust
1,230
star
35

spacecommander

Commit fully-formatted Objective-C as a team without even trying.
Objective-C
1,126
star
36

square.github.io

A simple, static portal which outlines our open source offerings.
CSS
1,108
star
37

workflow

A Swift and Kotlin library for making composable state machines, and UIs driven by those state machines.
Shell
1,107
star
38

workflow-kotlin

A Swift and Kotlin library for making composable state machines, and UIs driven by those state machines.
Kotlin
973
star
39

certigo

A utility to examine and validate certificates in a variety of formats
Go
917
star
40

logcat

I CAN HAZ LOGZ?
Kotlin
893
star
41

radiography

Text-ray goggles for your Android UI.
Kotlin
831
star
42

whorlwind

Makes fingerprint encryption a breeze.
Java
819
star
43

dagger-intellij-plugin

An IntelliJ IDEA plugin for Dagger which provides insight into how injections and providers are used.
Java
798
star
44

cycler

Kotlin
793
star
45

Paralayout

Paralayout is a set of simple, useful, and straightforward utilities that enable pixel-perfect layout in iOS. Your designers will love you.
Swift
771
star
46

apropos

A simple way to serve up appropriate images for every visitor.
Ruby
766
star
47

shift

shift is an application that helps you run schema migrations on MySQL databases
Ruby
735
star
48

coordinators

Simple MVWhatever for Android
Java
703
star
49

subzero

Block's Bitcoin Cold Storage solution.
C
667
star
50

Blueprint

Declarative UI construction for iOS, written in Swift
Swift
659
star
51

shuttle

String extraction, translation and export tools for the 21st century. "Moving strings around so you don't have to"
Ruby
657
star
52

gifencoder

A pure Java library implementing the GIF89a specification. Suitable for use on Android.
Java
654
star
53

pollexor

Java client for the Thumbor image service which allows you to build URIs in an expressive fashion using a fluent API.
Java
633
star
54

intro-to-d3

a D3.js tutorial
CSS
603
star
55

kochiku

Shard your builds for fun and profit
Ruby
602
star
56

curtains

Lift the curtain on Android Windows!
Kotlin
570
star
57

RxIdler

An IdlingResource for Espresso which wraps an RxJava Scheduler.
Java
512
star
58

svelte-store

TypeScript
494
star
59

field-kit

FieldKit lets you take control of your text fields.
JavaScript
463
star
60

burst

A unit testing library for varying test data.
Java
462
star
61

SuperDelegate

SuperDelegate provides a clean application delegate interface and protects you from bugs in the application lifecycle
Swift
454
star
62

otto-intellij-plugin

An IntelliJ IDEA plugin to navigate between events posted by Otto.
Java
453
star
63

js-jose

JavaScript library to encrypt/decrypt data in JSON Web Encryption (JWE) format and to sign/verify data in JSON Web Signature (JWS) format. Leverages Browser's native WebCrypto API.
JavaScript
424
star
64

sharkey

Sharkey is a service for managing certificates for use by OpenSSH
Go
390
star
65

connect-api-examples

Code samples demonstrating the functionality of the Square Connect API
JavaScript
381
star
66

fdoc

Documentation format and verification
Ruby
379
star
67

ETL

Extract, Transform, and Load data with Ruby
Ruby
377
star
68

lgtm

Simple object validation for JavaScript.
JavaScript
366
star
69

laravel-hyrule

Object-oriented, composable, fluent API for writing validations in Laravel
PHP
341
star
70

in-app-payments-flutter-plugin

Flutter Plugin for Square In-App Payments SDK
Objective-C
332
star
71

papa

PAPA: Performance of Android Production Applications
Kotlin
331
star
72

pysurvival

Open source package for Survival Analysis modeling
HTML
319
star
73

pylink

Python Library for device debugging/programming via J-Link
Python
317
star
74

workflow-swift

A Swift and Kotlin library for making composable state machines, and UIs driven by those state machines.
Swift
302
star
75

rails-auth

Modular resource-based authentication and authorization for Rails/Rack
Ruby
288
star
76

cocoapods-generate

A CocoaPods plugin that allows you to easily generate a workspace from a podspec.
Ruby
272
star
77

inspect

inspect is a collection of metrics gathering, analysis utilities for various subsystems of linux, mysql and postgres.
Go
267
star
78

Aardvark

Aardvark is a library that makes it dead simple to create actionable bug reports.
Objective-C
257
star
79

jetpack

jet.pack: package your JRuby rack app for Jetty.
Ruby
249
star
80

gradle-dependencies-sorter

A CLI app and Gradle plugin to sort the dependencies in your Gradle build scripts
Kotlin
242
star
81

luhnybin

Shell
232
star
82

auto-value-redacted

An extension for Google's AutoValue that omits redacted fields from toString().
Java
211
star
83

protoparser

Java parser for .proto schema declarations.
Java
209
star
84

squalor

Go SQL utility library
Go
203
star
85

p2

Platypus Platform: Tools for Scalable Deployment
Go
196
star
86

mimecraft

Utility for creating RFC-compliant multipart and form-encoded HTTP request bodies.
Java
195
star
87

Listable

Declarative list views for iOS apps.
Swift
189
star
88

git-fastclone

git clone --recursive on steroids
Ruby
185
star
89

zapp

Continuous Integration for KIF
Objective-C
179
star
90

metrics

Metrics Query Engine
Go
170
star
91

ruby-rrule

RRULE expansion for Ruby
Ruby
168
star
92

quotaservice

The purpose of a quota service is to prevent cascading failures in micro-service environments. The service acts as a traffic cop, slowing down traffic where necessary to prevent overloading services. For this to work, remote procedure calls (RPCs) between services consult the quota service before making a call. The service isn’t strictly for RPCs between services, and can even be used to apply quotas to database calls, for example.
Go
154
star
93

wire-gradle-plugin

A Gradle plugin for generating Java code for your protocol buffer definitions with Wire.
Groovy
153
star
94

goprotowrap

A package-at-a-time wrapper for protoc, for generating Go protobuf code.
Go
148
star
95

beancounter

Utility to audit the balance of Hierarchical Deterministic (HD) wallets. Supports multisig + segwit wallets.
Go
143
star
96

womeng_handbook

Everything you need to start or expand a women in engineering group in your community.
129
star
97

cocoapods-check

A CocoaPods plugin that shows differences between locked and installed Pods
Ruby
126
star
98

rce-agent

gRPC-based Remote Command Execution Agent
Go
125
star
99

spincycle

Automate and expose complex infrastructure tasks to teams and services.
Go
118
star
100

in-app-payments-react-native-plugin

Objective-C
116
star