Swiftpack.co - bealex/Macaroni as Swift Package

Swiftpack.co is a collection of thousands of indexed Swift packages. Search packages.
bealex/Macaroni
Swift Dependency Injection Framework "Macaroni"
.package(url: "https://github.com/bealex/Macaroni.git", from: "v2.1.0")

Macaroni

Swift Dependency Injection Framework "Macaroni".

Main reason to exist

When I start my projects, I need some kind of DI. When property wrappers were introduced, it was obvious that this feature can be used for DI framework. So here it is.

Macaroni v.2 uses a hack from this article https://www.swiftbysundell.com/articles/accessing-a-swift-property-wrappers-enclosing-instance/ to be able to access self of the enclosing object. There is a limitation because of that: @Injected can be used only in classes, because properties are being lazy initialized when accessed first time.

Migration

Installation

Please use Swift Package Manager. Repository address: git@github.com:bealex/Macaroni.git. Name: Macaroni.

Simple example

First let's import Macaroni and prepare our protocol and implementation that we want to inject.

import Macaroni

protocol MyService {}
class MyServiceImplementation: MyService {}

Now let's register the service inside a dependency injection container (Macaroni.Container). Think of Container as a box that holds all objects for injection. We will use simple, service locator DI policy (other policies are described later).

func configure() {
    let container = Container()

    // This variant will create singleton resolver:
    let myService = MyServiceImplementation()
    container.register { myService }
    
    // And this object will be created every time during injection:
    container.register { MyServiceImplementation() }
    
    Container.policy = .singleton(container)
}

Please note that type of myService is inferred. This is why it will be able to inject it as MyServiceImplementation, but not MyService. To enable the latter, you should specify it either at declaration:

let myService: MyService = MyServiceImplementation()

Or during the registration:

container.register { () -> MyService in myService }

Now we can inject things!

class MyController {
    @Injected
    var myService: MyService
}

Please note that injection will happen lazily, not during MyController initialization but when myService is first accessed.

Using information about enclosing object (parametrized injection)

If you need to use object that contains injected property, you can get it inside registration closure like this:

container.register { enclosingObject -> String in String(describing: enclosing) }

Please note that there will be enclosed object only for @Injected and @InjectedWeakly Macaroni implementations. If you use Container in some custom environment, you are responsible for what is put there and how to use it. Macaroni property wrappers @Injected and @InjectedWeakly are using it only this way.

@Injected resolve procedure

You can see all logic in ResolvePolicy.swift.

If there is no value stored for the field, then it is created:

  • First, it looks for the parametrized resolver
  • Next, non-parametrized resolver
  • If nothing found, fatal error happens. You can override this behavior with the property: Macaroni.handleError

If you will simultaneously register parametrized type resolver with non-parametrized one, parametrized will take precedence.

DI Policies

There are three policies of container selection for properties of specific enclosing object:

  • service locator style. It is called .singleton, and can be set up like this: Container.policy = .singleton(myContainer).
  • enclosing object based. This policy implies, that every enclosing object implements WithContainer protocol and contains its own Container because of that. You can set it up like this: Container.policy = .enclosingObjectWithContainer or if you want to use default singleton container for all objects that do not implement WithContainer, you can set default one: Container.policy = .enclosingObjectWithContainer(defaultSingletonContainer: myDefaultContainer)
  • custom. If you want to control container selection yourself and no other options help you, you can set it like this: Container.policy = .custom { enclosingObject in /* your container selection policy */ }

Weak injection

When using property wrappers, you can't use weak (or lazy or unowned). If you need that, you can use @InjecteadWeakly instead of @Injected. Here is an example:

private protocol MyService: AnyObject {}
private class MyServiceImplementation: MyService {}

private class MyController {
    @InjectedWeakly
    var myService: MyService?
}

// Please note that registration should not strongly capture an object. You should do something like this
let service: MyService = MyServiceImplementation()
let container = Container()
container.register { [weak service] in service }
Container.policy = .singleton(container)

Init-time injection

Second version of @Injected property wrapper is lazy. This means that objects are injected on first use, not during enclosing object initialization time.

If you still need simple, "version-one style" DI, you can use @InjectedFrom(container: Container).

Per Module Injection

If your application uses several modules and each module needs its own Container, you can use this option:

// Common code:
protocol ModuleDI: WithContainer {}

// Using this policy for each object to decide what container to use.  
Container.policy = .enclosingObjectWithContainer()

// Module specific code:
// In each module create a container, and fill it with needed resolvers.
private var moduleContainer: Container!
// Extension is internal. This way each module can have its own 
extension ModuleDI {
    var container: Container! { moduleContainer }
}

// And we can "inject" container like this.
class MyCoordinator: ..., ModuleDI, ... {
    ...
}

Alternatives

Sometimes you need to register several objects of the same type. In Macaroni they are called RegistrationAlternative and can be registered like this:

protocol ToInject {}

extension RegistrationAlternative {
    static let first: RegistrationAlternative
    static let second: RegistrationAlternative
}

container.register(alternative: .first) { () -> ToInject in toInjectFirstObject }
container.register(alternative: .second) { () -> ToInject in toInjectSecondObject }

And inject it like this:

@Injected(alternative: .first)
var firstValue: ToInject

@Injected(alternative: .second)
var secondValue: ToInject

You still have an ability to register default object together with other alternatives, that will be accessible via simple @Injected without parameters.

Multithreading support

Macaroni does not do anything about multithreading. Please handle it yourself if needed.

Logging

By default, Macaroni will log simple events: containers creation and resolvers registering. If you don't need that (or need to alter logs in some way), please use:

// To disable debug logs:
Macaroni.logger = DisabledMacaroniLogger()

// To alter logging behavior:
class MyMacaroniLogger: MacaroniLogger {
    // Implement logging methods
}

Macaroni.logger = MyMacaroniLogger()

GitHub

link
Stars: 4
Last commit: 1 week ago

Ad: Job Offers

iOS Software Engineer @ Perry Street Software
Perry Street Software is Jack’d and SCRUFF. We are two of the world’s largest gay, bi, trans and queer social dating apps on iOS and Android. Our brands reach more than 20 million members worldwide so members can connect, meet and express themselves on a platform that prioritizes privacy and security. We invest heavily into SwiftUI and using Swift Packages to modularize the codebase.

Submit a free job ad (while I'm testing this). The analytics numbers for this website are here.

Release Notes

Removed example from SPM to avoid conflicts
1 year ago

Swiftpack is being maintained by Petr Pavlik | @ptrpavlik | @swiftpackco | API | Analytics