Giter VIP home page Giter VIP logo

cleanse's Introduction

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

cleanse's People

Contributors

0xflotus avatar abdullahselek avatar binlogo avatar chathil avatar chrissonoda avatar codydunlap avatar dependabot[bot] avatar dfed avatar djben avatar holmes avatar jbeachwood avatar mikelikespie avatar pietrocaselani avatar sebastianv1 avatar solidcell avatar yelite avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

cleanse's Issues

Property Injections

I'm trying to resolve properties of my objects, but in one of them injectProperties method does not call.

Here is my code:

final class AppDelegate: UIApplicationDelegate {
        
    private var repository: Repository!
    private var view: View!
    
    private func inject() {
        let propertyInjector: PropertyInjector<AppDelegate> = try! ComponentFactory.of(AppComponent.self, validate: true).build(())
        propertyInjector.injectProperties(into: self)
    }
    
    func injectProperties(_ repository: Repository, view: View) {
        self.repository = repository
        self.view = view
        
        repository.exec()
    }
    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
        inject()
        return true
    }
}

class View {
    
    let presenter: Presenter
    
    init(presenter: Presenter) {
        self.presenter = presenter
    }
}

class Presenter {
    
    weak var view: View? 
    
    init() {
    }
    
    func injectProperties(_ view: View) {
        self.view = view
    }
}

class Executor {
}

protocol Repository {
    func exec()
}

class RepositoryImpl: Repository {
    
    let executor: Executor
    
    init(executor: Executor) {
        self.executor = executor
    }
    
    func exec() {
        debugPrint("exec")
    }
}

struct UserScope: Scope {}
struct ApplicationScope: Scope {}

struct CoreModule: Module {
    typealias Scope = UserScope
    
    static func configure(binder: Binder<UserScope>) {
        binder.bind().to(factory: Executor.init)
        binder.bind(Repository.self).to(factory: RepositoryImpl.init)
    }
}

struct AppComponent: RootComponent {
    typealias Root = PropertyInjector<AppDelegate>
    typealias Scope = UserScope
    
    static func configure(binder: Binder<AppComponent.Scope>) {
        binder.include(module: CoreModule.self)
        
        binder.bind().to(factory: View.init)
        binder.bind().to(factory: Presenter.init)
        // looks like it do nothing
        binder.bindPropertyInjectionOf(Presenter.self).to(injector: Presenter.injectProperties)
    }
    
    static func configureRoot(binder bind: ReceiptBinder<AppComponent.Root>) -> BindingReceipt<AppComponent.Root> {
        return bind.propertyInjector(configuredWith: { (bind) -> BindingReceipt<PropertyInjector<AppDelegate>> in
            bind.to(injector: AppDelegate.injectProperties)
        })
    }
}

So, my questions is what I'm doing wrong? why Presenter.injectPoperties does not call?

Swift3 - Why no more explicit overrideModule

I used to have one MainModule with all my bindings inside and it was quite simple to override with Mock in my tests cases. Like:

propertyInjector = try! Component().withOverrides(overrideModule: FakeModule()).build()

And within my FakeModule

binder
    .bind(Service.self)
    .to(value: MockService())

Now I just can't do that anymore because of Already bound exception occurring when I install my MainModule and then try to override like I used to.

Is the only solution to split MainModule to more specifics <Specific>Module and only override the ones I need like you do in AppComponent.swift or is there another way to override ?

[RFC] Assisted Injection Feature

Cleanse: Assisted Inject

Author: sebastianv1
Date: 9/4/2019
Related Links:
https://github.com/google/guice/wiki/AssistedInject


Background

Factories that combine parameterization (or a seed value) and pre-bound dependencies are commonplace in any piece of software that derives its class variables from a particular state. For example, consider a customer list view that drills into a customer detail page. The detail view controller initializer may look like:

class CustomerDetailViewController: UIViewController {
    private let customerID: String
    private let customerService: CustomerService

    init(customerID: String, customerService: CustomerService) {
      self.customerID = customerID
      self.customerService = customerService
    }
    ...
}

CustomerService in this example is a pre-bound dependency that can be constructed independently from CustomerDetailViewController. Our class variable customerID on the other hand is a parameter passed in from the list view that is dependent on which cell the user selected.

Today, Cleanse supports constructing subcomponents that can inject in a seed value, but doesn't have support for constructing a dependency from a seed in the same component. If one wanted to achieve the effect described in the example above, they would have to create a new factory object that injects all of the target object's dependencies, and create a custom build(_:) function.

class CustomDetailViewControllerFactory {
  let customerService: CustomerService

  // seed is just a String representing `customerID`
  func build(_ seed: String) -> CustomerDetailViewController {
    return CustomerDetailViewController(customerID: seed, customerService: customerService)
  }
}

extension CustomDetailViewControllerFactory {
  struct Module: Cleanse.Module {
    public static func configure(binder: UnscopedBinder) {
      binder
        .bind(CustomDetailViewControllerFactory.self)
        .to(factory: CustomDetailViewControllerFactory.init)
    }
  }
}

This creates a large amount of unnecessary boilerplate that Cleanse can help eliminate without needing to create a whole new subcomponent.

Proposed Solution

To eliminate the unnecessary boilerplate required, Cleanse will support a new binding builder whose purpose is to support Assisted Inject.

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

  struct Module: Cleanse.Module {
    public static func configure(binder: UnscopedBinder) {
      binder
        .bindFactory(CustomerDetailViewController.self)
        .with(AssistedSeed.self)
        .to { (seed: Assisted<String>, customerService: CustomerService) in
          return CustomerDetailViewController(
            customerID: seed.get(),
            customerService: customerService)
        }
    }
  }
}

The above binding will create a Factory<CustomerDetailViewController.AssistedSeed> instance that can be used in the dependency graph. For instance, our customer detail list view could look like:

class CustomerListViewController: UIViewController {
  let customerDetailFactory: Factory<CustomerDetailViewController.AssistedSeed>
  ...

  func tappedCustomer(with customerID: String) {
    let customerDetailViewController = customerDetailFactory.build(customerID)
    self.present(customerDetailViewController, animated: true)
  }
}

Detailed Design

Factory<Tag: AssistedFactory>

public struct Factory<Tag: AssistedFactory> {
    let factory: (Tag.Seed) -> Tag.Element
    public func build(_ seed: Tag.Seed) -> Tag.Element {
        return factory(seed)
    }
}

This is a very lightweight and simple class. It's primary purpose is exposing the public API build(:) and passing the provided seed parameter into the factory closure.

Assisted<E>

The Assisted object wraps the seed value that is injected via the build(:) function from our factory.

public struct Assisted<E> : ProviderProtocol {
    public typealias Element = E

    let getter: () -> Element
    public init(getter: @escaping () -> E) {
        self.getter = getter
    }

    public func get() -> E {
        return getter()
    }
}

The purpose of Assisted is primarily annotative to make it more explicit that these values
are provided via the assisted inject mechanism, similar to Guice's @Assisted annotation. Wrapping the seed inside the Assisted instead of directly using the seed type adds helpful transform functions (i.e map) and can be used to create more succinct bindings leveraging Swift's type inference. More on this later.

protocol AssistedFactory

public protocol AssistedFactory: Tag {
    associatedtype Seed
}

AssistedFactory inherits from the protocol Tag, which makes it expand to:

public protocol AssistedFactory: Tag {
    associatedtype Seed
    // From Tag
    associatedtype Element
}

Utilizing a tagging system for assisted injection make the surface area of changes required smaller and easier to spot when adding or changing an assisted inject object.

This point is easier to show if we removed the tagging system. So instead of Factory having a generic over an AssistedFactory instance, let's say we turned it into Factory<E, S> where E and S are the same as Element and Seed respectively from the AssistedFactory protocol, and had our builder function with(:) take in the raw types.

Consider the following assisted injection class:

struct CoffeeMachine {
  let bean: Bean
  let handle: CoffeeHandle
  let maxWeight: Int

  init(bean: Bean, handle: CoffeeHandle, maxWeight: Int) {
    ...
  }
}

We would create our binding as such:

extension CoffeeMachine {
  public struct Module: Cleanse.Module {
    public static func configure(binder: UnscopedBinder) {
      binder
        .bindFactory(CoffeeMachine.self)
        .with((Bean.self, CoffeeHandle.self, Int.self))
        .to { (seed: Assisted<(Bean, CoffeeHandle, Int)>) in
          let (bean, coffeeHandle, weight) = seed.get()
          return CoffeeMachine(bean: bean, coffeeHandle: coffeeHandle, weight: weight)
        }
    }
  }
}

And our injection would be:

struct Shop {
  let coffeeMachineFactory: Factory<CoffeeMachine, ((Bean, CoffeeHandle, Int))>
  ...
}

If we were to change the Seed by adding another parameter (say a new String), we would have to change every injection of Factory<CoffeeMachine, ((Bean, CoffeeHandle, Int))> to Factory<CoffeeMachine, ((Bean, CoffeeHandle, Int, String))> in addition to any semantical changes required. This can make refactors and changes burdensome and annoying.

Assisted Injector Builder Objects

The entry point into our assisted injector builder is through the bindFactory(_:) function:

extension BinderBase {
    public func bindFactory<Element>(_ class: Element.Type) -> AssistedInjectionBindingBuilder<Self, Element> {
        return AssistedInjectionBindingBuilder(binder: self)
    }
}

AssistedInjectionBindingBuilder is a type that conforms to BaseAssistedInjectorBuilder

public protocol BaseAssistedInjectorBuilder {
    associatedtype Binder: BinderBase
    associatedtype Tag: AssistedInjector = EmptySeed<Element>
    associatedtype Element
    var binder: Binder { get }
}

The default type for Tag is EmptySeed<Element> whose Seed = Void, meaning that the binding builder with(:) is actually an optional builder step. This means that if one uses assisted injection
without a seed, the resulting Factory type expands to Factory<Element, Void>.

Terminating Step and Generated Arity Code

The terminating builder step for an assisted injection will have 2 functions. For example, the 1st-arity function will look like:

@discardableResult public func to<P_1>(file: StaticString=#file, line: Int=#line, function: StaticString=#function, factory: @escaping (Assisted<Tag.Seed>, P_1) -> Element) -> BindingReceipt<Factory<Tag>>

and

@discardableResult public func to<P_1>(file: StaticString=#file, line: Int=#line, function: StaticString=#function, factory: @escaping (P_1, Assisted<Tag.Seed>) -> Element) -> BindingReceipt<Factory<Tag>>

Note the difference between factory parameters, where the Assisted<Tag.Seed> comes at the beginning of one, and the end of the other. The reasoning behind why we are choosing to provide two functions is explained more in the Swift Type Inference section.

Generated arity-code

Similar to property injection and constructor injection, assisted injection will also generate (1,n] functions to support a variadic number of injected dependencies. This code will also live in main.swift for the CleanseGen target.

Error Handling

Assisted injection only supports 1 unique binding per AssistedFactory tag. Any additional bindings will throw an exception.

It is possible however, to create create a constructor and assisted injection binding for the same type. For example both of the following are allowed:

struct Coffee {
  let name: String
}

struct CoffeeModule : Cleanse.Module {
  struct CoffeeAssistedInject : AssistedInject {
    typealias Element = Coffee
    typealias Seed = String
  }

  static func configure(binder: UnscopedBinder) {
    // Both of these bindings are valid and will pass validation.
    binder
      .bind(Coffee.self)
      .to { Coffee(name: "Hello") }


    binder
      .bindFactory(Coffee.self)
      .with(CoffeeAssistedInject.self)
      .to { (seed: Assisted<CoffeeAssistedInject.Seed>) in
        return Coffee(name: seed.get())
      }
  }
}

The difference lies in the final types that are bound into the object graph. In the first case, an instance of Coffee.self is bound into the object graph, in the second an instance of Factory<CoffeeAssistedInject>.self is bound into the object graph.

As of this proposal, it is not possible to create tagged bindings for assisted injector objects for binding different instances of the same factory.

Swift Type Inference

Cleanse's API was implemented to leverage Swift's type inference via the binding arity functions we generate. For instance, we can create a succinct binding based on the init function like:

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

When it comes to assisted injection, we can still leverage the type inference system if we include the Assisted<Seed> parameter in our initializer:

struct Coffee {
  let name: Assisted<String>
}

extension Coffee {
  struct CoffeeAssisted: AssistedInject {
    typealias Element = Coffee
    typealias Seed = String
  }
  struct Module : Cleanse.Module {
    static func configure(binder: UnscopedBinder) {
      binder
        .bindFactory(Coffee.self)
        .with(CoffeeAssisted.self)
        .to(factory: Coffee.init)
    }
  }
}

However, it's important to note that the Assisted<Seed> parameter must either come first or last in the initializer function to leverage the type inference from the generated arity functions. This is because if we allowed Assisted<Seed> to go anywhere in the initializer, then the number of functions required to generate would grow exponentially, slowing down the generator and bloating up the binary size.

Revisions

  • [9/4] Initial Draft.
  • [9/5] Rename AssistedInjector to AssistedFactory and changed the initial binding function from bindAssisted to bindFactory.

InjectProperties from one component to multiple object types

Hey yall! I'm trying to add Cleanse to my app to split out debug-only code from the release build. I'm trying to minimize my changes at first, so I'm using property injection (yes, I read the "property injection is a second-class citizen section of the readme).

Here's the component I've defined:

struct FooComponent : Cleanse.Component {
	typealias Root = PropertyInjector<AppDelegate>
	
	func configure<B : Binder>(binder binder: B) {
		#if DEBUG
			binder.install(module: DebugModule())
		#else
			binder.install(module: ReleaseModule())
		#endif
		binder.bindPropertyInjectionOf(AppDelegate.self).to(injector: AppDelegate.injectProperties)
		binder.bindPropertyInjectionOf(RequestViewController.self).to(injector: RequestViewController.injectProperties)
	}
}

And I'm installing it here in my AppDelegate (this works great):

		component = try! FooComponent().build()
		component.injectProperties(into: self)

However, if I try to injectProperties on a RequestViewController, the compiler blows up (Cannot convert value of type 'RequestViewController' to expected argument type 'AppDelegate')

func inject(obj: RequestViewController) {
	component.injectProperties(into: obj)
}

This makes sense because Component.build() only returns a PropertyInjector<AppDelegate> Can I somehow get access to the underlying graph? From looking at the source of Component.build(), it builds the graph and then just returns the PropertyInjector<AppDelegate>, throwing away the graph 😭

Compiler issues with latest version of XCode and Swift 4

We are using Swift 4 in our app and updating to the latest version of XCode with Swift 5 support seems to have introduced some new issues with Cleanse.

It appears to be similar to #83

In the configure function of a Module with a binder like binder.bind().to(factory: MainViewController.init) , we get an error message ambiguous use of 'init'

And when we use the binder.bind().to { some code } function, we also get a compiler error ambiguous use of 'to(file:line:function:factory:)'

We've tried updating to the latest version of the Cleanse library, 4.2.3 at the time of this issue, and have tried the changes mentioned in the referenced issue above. Both have had no effect.

Is there a known/recommended resolution or work-around for this?

Question: injection with arguments

Thanks for the awesome library!
README says that assisted injection is not supported. I was wondering, what is the recommended solution for it until it is supported? Should I create factories and write bindings for them or there is a better pattern?
Interface + implementation + factory + module sounds like a lot of code.
Thanks!

Sharing "Singleton" across scope and object graph

Given these two root components with the following scope:

struct ViewScope: Scope {}
typealias ViewBinder = Binder<ViewScope>

extension FirstViewController {
    struct Component: RootComponent {

        typealias Root = FirstViewController
        typealias Seed = Void
        typealias Scope = ViewScope

        static func configure(binder: ViewBinder) {
            binder.include(module: ViewModule.self)
        }

        static func configureRoot(binder bind: ReceiptBinder<Root>) -> BindingReceipt<Root> {
            return bind.to1(factory: FirstViewController.init(videoView:)
        }
    }
}
extension SecondViewController {
    struct Component: RootComponent {

        typealias Root = SecondViewController
        typealias Seed = Void
        typealias Scope = ViewScope

        static func configure(binder: ViewBinder) {
            binder.include(module: ViewModule.self)
        }

        static func configureRoot(binder bind: ReceiptBinder<Root>) -> BindingReceipt<Root> {
            return bind.to1(factory: SecondViewController.init(videoView:)
        }
    }
}

where ViewModule looks like:

struct ViewModule: Module {
    static func configure(_ binder: ViewBinder) {
        binder
            // some view class like a video view that should be a singleton object
            .bind(SomeCustomVideoPlayerAVFoundationView.self)
            .sharedInScope()
            .to0(factory: SomeCustomVideoPlayerAVFoundationView.init)
    }
}

it would seem that if I built instances of FirstViewController and SecondViewController that the videoView property on both would refer to the same instance. That doesn't seem to be the case though, it looks like singletons or objects created via sharedInScope are restricted not just to scope but also to the object graph that's associated with the root component. That would make sense; I'm creating two new and distinct graphs when I create two separate root components.

The question really is, how do I share object graphs between root components? Graph has a init(scope:parent:) but that's an internal class. Does a public mechanism exist to do that?

Tangentially related: how do you build Component (vs RootComponent)? Both are defined as empty protocols conforming to ComponentBase but only RootComponent can be built directly using ComponentFactory. In that case, is Component useful? It seems in the case that I would binder.install(SomeComponent.self) that I would just make it a module and include it.

Improve Error Handling for Duplicate Bindings

If one bind the same type twice, it will fail immediately when the duplicate dependency is seen. This isn't terrible because the stack trace will show you where the second declaration is. However, the error messages don't show where the other binding is. This could be improved.

Crash when injecting an optional protocol

Hello,

I faced a crash while playing with Cleanse, when I tried to inject an optional protocol.
The crash can be reproduced in a XCode 7.3 playground with the code below :

protocol FooProto {
}

struct Foo : FooProto {
}

struct Bar {
    var foo : FooProto?
    init(foo: FooProto?) {
        self.foo = foo
    }
}

struct BarComponent : Cleanse.Component {
    typealias Root = Bar

    func configure<B : Binder>(binder binder: B) {
        binder
            .bind(Bar.self)
            .to(factory: Bar.init)
        binder
            .bind(FooProto.self)
            .to(factory: Foo.init)
    }
}

let bar = try! BarComponent().build()

The crash occurs in Provider.swift, in Provider.makeNew and AnyProvider.asCheckedProvider.
When downcasting from Any to Element (e.g getter() as! Element), you get the following exception :

Could not cast value of type 'Swift.Optional' to 'FooProto'

The problem is in fact on Swift side : for the moment, it's impossible to cast from T? to Any and then downcast from Any to some protocol implemented by T (while it's ok to downcast to T...) . I filed an issue for this problem in their backlog : SR-1999

Also, I came up with some workaround but it uses reflection to unwrap the Optional in Any... Not sure it would conform to the strong-typing spirit of Cleanse, though ^^

One could argue that there is no point in injecting optional parameters, but I think it's something which can really occur in real-world circumstances, for exemple when working with an external lib whose constructors take optional parameters.

Remove Compiler Warnings

Current state of the code is that it has warnings when compiled with both Swift 2.2 and Swift 3.0. This was so we could have code that compiles with both and not have to use too many #if swift(>=3.0) statements.

This should be cleaned up once swift 3 is officially released and we drop support for Swift 2.2

Add "Singleton"/scope Protocol to make "asSingleton" default

Some classes are inherently a singleton. One may forget to do .asSingleton() when configuring them currently. If we have a protocol called singleton which behaved similar to @Singleton annotation in java when used, we can make things better. This needs to be thought out a little more though since annotations in java and protocol conformance in swift have different semantics when it comes to inheritance, etc.

How should I solve cyclical dependencies in Cleanse?

Hi,

I'm using the swift-3 branch of Cleanse and have a question related to solving cyclical dependencies:

I have a case where ModuleA depends on ModuleB, and ModuleB depends on ModuleA.

In order to work around this cyclic dependency I thought making ModuleA depend on Provider<ModuleB> should work, but it does not and I still get the following error: *** ModuleA *** Dependency Cycle Detected

What is the right approach using Cleanse to work around this issue?

Any help would be most appreciated! Cheers :)

Add Podspec if desired

It may be desired to consume Cleanse via Cocoapods. If it is, a Podspec should be added. If somebody would like to take this on, adding a script that automatically updates the spec or tag appropriately or adding instructions to the README would be desirable.

Swift 3 support

Thanks for making this project open source. The ideas and concepts look solid and more promising than the existing DI frameworks for Swift.

From what I see in the source code, you already try to support Swift 3. However it seems that you target one of the early previews of Swift 3. The language has changed very much with the latest swift evolutions and because of that Cleanse doesn't work with the latest beta of Swift 3.

Are you planning to support Swift 3 in the near future?

Also, there seems to be some work where you have small corrections in the APIs (e.g. #27) but development seems to have stalled. Do you have some kind of roadmap or another document where you outline future development?

Scope Singleton

According to the readme on GitHub I should be able to scope a Binder as :

// Required function from Cleanse.RootComponent protocol.
static func configure(binder: Binder<Unscoped>) { }

Line 17 in Scope.swift also states the following:
/// Currently there are only two scopes, UnscopedandSingleton.

But it looks like Singleton is completely removed from the project. Can you update the documentation and let me know what I should use instead?

Clean up Code Generation

The code generation for the multiple Arities binding is very gnarly. It should be DRYed a lot.

We've had success in other projects using code generation that has a DSL like the following

    print("public static let descriptor: FileDescriptor =", trailing: "()") {
        for d in f.dependencies {
            print("\(d.swiftOuterStructName).initializeFile()")
        }
        print("return try! DescriptorPool.generatedPool.buildFile(descriptorProto)")
    }

How to solve dependency cycle / property injection doesn't seem to work.

I'm having issues with dependency cycle. My Coordinator needs a LoginViewController and my LoginViewController needs a Coordinator to function. But I haven't found a way to get this to work with constructor injection. I also tried propertyInject, but the propertyInjection method doesn't get called. Below you'll see an example of my implementation.

protocol HomeRoute {
   func routeHome()
}

protocol RegisterRoute {
   func routeRegister()
}

class Coordinator: HomeRoute, RegisterRoute {

   private let loginViewControllerProvider: Provider<LoginViewController>

   init(loginViewControllerProvider: Provider<LoginViewController>) {
      self. loginViewControllerProvider = loginViewControllerProvider
   }

   func routeHome() {
      // show Home
   }

   func routeRegister() {
      // show Register
   }
}
class LoginViewController {
   typealias Coordinator = HomeRoute & RegisterRoute
   
   private var coordinator: Coordinator

   init(coordinator: Coordinator) {
       self.coordinator = coordinator
   }

   // Tryout with propertyInjection (is not being called)
   func injectProperties(_ coordinator: Coordinator) {
       self.coordinator = coordinator
   }
}
extension Modules {
    struct App: Cleanse.Module {
        static func configure(binder: AppBinder) {
            // Bind Coordinator (BTW is there a way to bind multiple variations of protocols to the same Coordinator? e.g. my RegisterCoordinator might only need a Coordinator confirming to HomeRoute protocol but should get the same Coordinator as the Login)
            binder.bind(LoginViewController.Coordinator.self).sharedInScope().to(factory: ApplicationCoordinator.init)

            binder.bind().to(factory: LoginViewController.init)

            // When running the app, this doesn't seem to do anything.
            binder.bindPropertyInjectionOf(LoginViewController.self).to(injector: LoginViewController.injectProperties) 

        }  
    } 
}

Re-add logo to README

Seems like github & rst doesn't like resizing images. Once we find a good way to make a smaller image, re-add it to the README.

Cleanse does have a sweet logo though.

cleanse_logo

Dependencies between Modules

When you have two modules and both modules depend on another module, Cleanse will crash because the bindings for the third module are already bound.

I am using the version of the swift-3 branch which uses the static configuration of #27.

Minimal example:

class Foobar: RootComponent {

    public static func configure<B : Binder>(binder: B) {

        binder.install(module: Module1.self)
        binder.install(module: Module2.self)
        binder
            .bind(UIWindow.self)
            .to {
                return UIWindow()
            }
    }

    typealias Root = UIWindow

}


struct Module1: Module {
    public static func configure<B : Binder>(binder: B) {
        binder.install(module: NetworkingModule.self)
    }


}

struct Module2: Module {
    /// This is where configuring providers occurs.
    public static func configure<B : Binder>(binder: B) {
        binder.install(module: NetworkingModule.self)
    }
}

struct NetworkingModule: Module {
    /// This is where configuring providers occurs.
    public static func configure<B : Binder>(binder: B) {
        binder.bind(URLSession.self).to(value: URLSession.shared)
    }
}

This will fail with the following error:

fatal error: Already bound at NSURLSession: file /tmp/CleanseTests/Pods/Cleanse/Cleanse/Graph.swift, line 114

Is there another way of using a module setup where the dependencies are like in my example? In my opinion it is a very common structure for applications where you have small, interchangeable components. As far as I know this is possible with dagger2 and the @Module(includes=) annotation.

Cleanse is broken in Swift 3.2

A few silly little warnings.

But the big issue is when declaring a Component that has a Seed of Void.

Now when calling componentFactory.build() we're getting

/work/Cleanse2/CleanseTests/ComponentVisitorTests.swift:29:41: Missing argument for parameter #1 in call

I've tried passing Void but then it says

/work/Cleanse2/CleanseTests/ComponentVisitorTests.swift:29:40: Argument passed to call that takes no arguments

We'll have to find the magic incantation.

How are Singleton Binders declared?

I'm trying to use Binder in my project and it does not work. I only see UnscopedBinder. Can you explain what unscoped means and how to define things in singleton scope?

Readme is out-of-date

I am trying to learn and understand the Clean framework. The sample project is nice but not as detailed in explanations as the Readme. When I try to follow the Readme however, it doesn't work; it appears to be out-of-date. I would be happy to update it, but I don't understand the framework enough yet to do so.

Subcomponents: Are they the only way to configure components after launch?

Am I correct in assuming that the tentative Subcomponents would allow me to configure a component after the RootComponent has been configured?

The use case I'm trying to solve is to configure our UI based on A/B experiment data coming back from the BE, however thats some time after the Root Component has been created.

Would the solution be to then configure a subcomponent with the resulting A/B test assignments and instantiate the UI from that component?

At present, I cannot find any way to do this in the current Cleanse implementation after a reasonable search.

Unjust dependency cycle

Currently I am facing an issue where Cleanse stops the app due to a detected dependency cycle.
For example LoginViewController has a Provider<OnboardingViewController>
The OnboardingViewController has a Provider<LoginViewController>

This should not be an issue as long as the provider is used outside of the initialiser. I cannot think of a reason to use a Provider's .get() directly inside of an initialiser anyway. Is this cycle detection as designed or a bug?

Example project: https://github.com/TomVanDerSpek/cleanse-tryout-ios/tree/unjust_dependency_cycle_detection

Swift 4 DI framework alternatives?

Was cheering for the swift 4 support. bummer.
I hope #67 can be resolved soon 💪 🔢

Personal runner ups:
0️⃣ factor 1️⃣ factory 2️⃣ factoruple 3️⃣ factoryplet 4️⃣ factoruple 5️⃣ factopenta | pentafactory 6️⃣ hexafactory | factorhex

In the mean time, can anybody point me to a simple swift 4 compatible DI framework?

Remove "LegacyObjectGraph" (if desired)

The legacy object graph exists to support use of requesting objects in an objective-c environment. The primary purpose of this is to facilitate migration from an older, internal DI library, but may be generally useful.

Getting rid of this appendage will simplify the code a little bit.

Advantage of using Tags

This framework is fantastic but the one thing I can't wrap my head around is

In the example:

extension UIWindow {
  struct Module : Cleanse.Module {
    public func configure<B : Binder>(binder binder: B) {
      binder
        .bind(UIWindow.self)
        .asSingleton()
        .to { (rootViewController: TaggedProvider<UIViewController.Root>) in
          let window = UIWindow(frame: UIScreen.mainScreen().bounds)
          window.rootViewController = rootViewController.get()
          return window
        }
    }
  }
}

and

extension RootViewController {
  /// Configures RootViewController
  struct Module : Cleanse.Module {
    func configure<B : Binder>(binder binder: B) {
      // Configures the RootViewController to be provided by the initializer
      binder
        .bind()
        .to(factory: RootViewController.init)

      // This satisfies UIWindow depending on TaggedProvider<UIViewController.Root>
      // The actual root is our RootViewController wrapped in a UINavigationController
      binder
        .bind()
        .tagged(with: UIViewController.Root.self)
        .to { UINavigationController(rootViewController: $0 as RootViewController) }
    }
  }
}

are equivalent to:

  • using Provider<UINavigationController> instead of TaggedProvider<UIViewController.Root>
  • removing .tagged(with: UIViewController.Root.self)

What would the advantage of using tags be in this situation? Is it solely for API clarity?

Continuing with the api url example used in the readme, I constructed some possible use cases for tags (as I understand them):

extension NSURL {
    struct Primary: Tag {
        typealias Element = NSURL
    }
    struct Github: Tag {
        typealias Element = NSURL
    }
}

struct APIModule: Cleanse.Module {

    func configure<B: Binder>(binder binder: B) {
        binder
            .bind()
            .to(factory: APIModule.init)
        binder
            .bind()
            .tagged(with: NSURL.Primary.self)
             .to { NSURL(string: "https://myapi.com")!  }
}

struct GithubModule: Cleanse.Module {

    func configure<B: Binder>(binder binder: B) {
        binder
            .bind()
            .to(factory: GithubModule.init)
        binder
            .bind()
            .tagged(with: NSURL.Github.self)
            .to { NSURL(string: "https://github.com")! }
    }
}

Is that a correct usage of tags? It seems a bit contrived, distinguishing between plain NSURL instances but I can see there being a value in tagging more complex classes.

Using swift3 version in .podspec / release of 1.0 ?

I'm currently developing a framework using the swift-3 branch of Cleanse and I want to release it as a pod.

I saw that you're planning on releasing a major 1.0 of Cleanse once you migration to swift3 is ready like most of the libs I use which is great.

To release my pod I need to add Cleanse as dependency in my .podspec but it seems I can't specify a branch in this file. In consequences when I try to run pod repo push... the linter fails because it retrieves the swift2 version of Cleanse (0.1.0) and throws things like:

- ERROR | xcodebuild:  Cleanse/Cleanse/BindingBuilder.swift:111:32: error: '.dynamicType' is deprecated. Use 'type(of: ...)' instead
- ERROR | xcodebuild:  Cleanse/Cleanse/BindingBuilder.swift:141:17: error: expected ',' joining parts of a multi-clause condition
- ERROR | xcodebuild:  Cleanse/Cleanse/ComponentVisitor.swift:118:77: error: expected 'let' in conditional
[...]

I saw that you have 3 tags on your master, 0.1.0, 1.0.0-a.1 and 1.0.0-a.2 . I tried to specify one of the 1.0.0-a.x but sadly it's not working:

[!] Unable to satisfy the following requirements:
- `Cleanse (~> 1.0.0-a.2)` required by `Podfile`
None of your spec sources contain a spec satisfying the dependency: `Cleanse (~> 1.0.0-a.2)`.

And I think the reason is because your .podspec still refer to 0.1.0

Q: Are you planning to release the 1.0.0 as a pod soon ? Or am I missing something ?

It seems the only solution for me right now is to fork your swift-3 and use mine instead but I don't really want to do that.

You can find more information about my problem on this issue I created on the cocoapods project.

Rethink/Finalize Overrides

Cleanse has support for overriding components currently, but it is experimental. Dagger 2 chose to explicitly not support overrides. Do we want this behavior? Overrides should be thought out better at least before the API is finalized.

Question: Is there a better way to use constructor injection for ViewControllers?

So I have gone through all of example code that I can find on Cleanse, as well as the documentation. As my app can have rather deep navigation at times, I'm struggling with finding a clean solution to injecting dependencies into my view controllers. The problem I'm running into is that with the following scenario:

ViewControllerA - depends on Service1 - presents ViewControllerB
ViewControllerB - depends on Service1 and Service2 - presents ViewControllerC
ViewControllerC - depends on Service3 and Service4

I'm using constructor injection (not using storyboard), but because of the navigation, even though ViewControllerA and ViewControllerB have no dependency on Service3 and Service4, they end up having a dependency on them just so they can pass those dependencies down the navigation chain to ViewControllerC.

I've toyed with the idea of just using property injection for each of my ViewControllers like I do for AppDelegate and also like I would do if I were using Storyboard, but I really don't want to do that. Am I missing something? Is there a better way to manage this? Or this just something I have to deal with? Coming from Android with Dagger/Dagger2, this just feels wrong.

Not sure how to proceed with Swift3 project

Hi,

I'm trying to get something very simple working: a single component which installs a single module which provides an implementation for a single protocol.

Essentially, all I want to do is to provide an implementation of a protocol without having to manually inject (potentially many dependent objects) via constructors. Have I misunderstood that Cleanse can help with this?

I don't know how to go about a Swift 3 solution, as I'm finding it hard to find docs/examples featuring the new syntax for the swift-3 branch, but this is my attempt:

The root component and implementation

protocol MyRootProtocol
{
    func test()
}

struct MyRoot : MyRootProtocol
{
    let myService:MyServiceProtocol
    
    func test()
    {
        self.myService.echo(message: "hello world")
    }
}

struct SingletonScope : Cleanse.Scope {}

struct MyRootComponent : Cleanse.Component
{
    typealias Root = MyRootProtocol
    typealias Scope = SingletonScope
    
    //Install the modules we have made
    static func configure(binder: Binder<MyRootComponent.Scope>)
    {
        binder.include(module:MyServiceModule.self)
    }
    
    static func configureRoot(binder bind: ReceiptBinder<MyRootProtocol>) -> BindingReceipt<MyRootProtocol>
    {
        return bind.to(factory:
        {
            () -> MyRoot in
            return MyRoot()
        })
    }
}

The service module and implementation

protocol MyServiceProtocol
{
    func echo(message:String) -> Void
}

struct MyServiceImplementation : MyServiceProtocol
{
    func echo(message:String) -> Void
    {
        print("echo: \(message)")
    }
}

struct MyServiceModule : Cleanse.Module
{
    static func configure(binder: Binder<SingletonScope>)
    {
        binder
        .bind(MyServiceProtocol.self)
        .sharedInScope()
        .to
        {
            () -> MyServiceProtocol in
            return MyServiceImplementation()
        }
    }
}

Any help would be most appreciated! Cheers!

Building with Swift 4 Blocked on SR-6108

Reference: https://bugs.swift.org/browse/SR-6108

It appears there is a bug in Swift as a result of SE-0110 (stronger type safety for tuples) that affects our BinderArities methods. Cleanse fails to compile tests because of the following compiler error:

error: ambiguous use of 'to(file:line:function:factory:)'
            binder.bind().to(factory: StructWithDependencies.init)
                   ^
Cleanse.BindToable:9:17: note: found this candidate
    public func to<P_1>(file: StaticString = #file, line: Int = #line, function: StaticString = #function, factory: @escaping (P_1) -> Self.Input) -> Cleanse.BindingReceipt<Self.Input>
                ^
Cleanse.BindToable:37:17: note: found this candidate
    public func to<P_1, P_2, P_3, P_4, P_5>(file: StaticString = #file, line: Int = #line, function: StaticString = #function, factory: @escaping (P_1, P_2, P_3, P_4, P_5) -> Self.Input) -> Cleanse.BindingReceipt<Self.Input>

For some reason, swift cannot disambiguate between the two generic functions. There is a workaround listed in the JIRA from the swift project, that will resolve the issue if the parameter names of each generic function are unique. So in our case, changing factory: @escaping... to something more unique for each arity such as factory1: @escaping..., factory2: @escaping.

I don't think this is an acceptable solution for Cleanse so we may have to get creative on this one.
Thoughts @holmes @mikelikespie

Have TravisCI test everything

This means testing swift 2.2 and open source swift 3.0 as well as running tests for example projects to make sure they don't regress.

Swift3 example

Would be great to have example for Swift3.
The default example results in Type AppDelegate.Component doesn't conform to protocol ComponentBase forcing to override static funcs

Installing through CocoaPods returns "Unable to find a specification"

Adding pod 'Cleanse', '~> 0.1' returns [!] Unable to find a specification for 'Cleanse (~> 0.1)'
Adding pod 'Cleanse' returns [!] Unable to find a specification for 'Cleanse'

I can see that there's a spec on CocoaPods GitHub, but there's no instruction in readme

Pods version 1.0.1

Add Cycle Validation

Right now, if there's a dependency cycle, one will get a stack overflow at runtime. This isn't great and needs to be fixed.

When implementing this feature, we need to be extra careful to surface actionable error messages since debugging a cycle can be very hard.

Is this project dead?

Not a lot of action in here in some time. Just wondering if I should look elsewhere for a Swift DI framework (answer: probably). Thanks!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.