Effortless modular dependency injection for Swift.

Overview

Inject

Effortless modular dependency injection for Swift.

Unit Tests


Sometimes during the app development process we need to replace instances of classes or actors we use in production code with instances that emulate their work e.g. tests, SwiftUI previews, demo apps etc.

Ususally that requires additional changes in the code that in turn opens up a whole new layer of errors. handling of theese errors is on your shoulders.

Inject lets you express your intent in a way that enables compile-time checking that you have all the instances required for the production. At the same time it let's you replace the instance on any object with a single line of code.

How to Use

Here's an example of a class that needs networking and parser instances to fetch items from the server:

final class BackendSDK {

    @Injected(\.networking) var network
    @Injected(\.parser) var parser

    func fetchItems() async throws -> [Item] {
        guard let url = URL(string: baseURL + "/items")
        else { throw InvalidURL() }
        let data = try await network.instance.fetch(url)
        return try await parser.instance.parse(data, as: [Item].self)
    }
}

And here's an example of replacing one of the services with a mock in SwiftUI preview:

struct SomeView: View {
    @Injected(\.networking) var network
    ...
}

extension SomeView: Injectable {}

struct SomeView_Previews: PreviewProvider {
    static var previews: some View {
        SomeView()
            .injecting(MockNetworking(), for: \.network)
    }
}

With this convenient property wrapper @Injected you define a dependency requirement in a declarative way:

  • \.networking is the KeyPath to the instance to be obtained at \DefaultValues.networking.

  • network is the name of our injection point that we use to inject in preview .injecting(MockNetworking(), for: \.network). It behaves just like a normal variable would, with one exception, instead of providing an instance it provides a Dependency<T> wrapper, that has a computed property .instance to obtain an actual instance of type T.

  • MockNetworking - A class that we use only in tests or previews, that might simulate the network.

  • Note: We have to mark our view with an empty protocol Injectable to enable the injecting(_:_:) function.

That's it, you are done with dependency injection without a need to know what exactly that is.

The only thing that is missing is to tell the compiler what are the default values for our \.networking and \.parser dependencies. And that's where DefaultValues come in handy.

Default Values

Unlike other popular solutions Inject doesn't have a container instead it provides you with a DefaultValues class to extend with computed properties.

You never need to create an instance of this class, all you need to do is to extend it with the variable of the type of the dependency it represents and return a default implementation for it:

extension DefaultValues {
    /// Default instance for Networking
    var networking: Networking {
        HTTPNetworking()
    }

    /// Default instance for Parser
    var parser: Parser {
        JSONParser()
    }
}

If you noticed networking and parser are the names we referred to earlier.

Dependency configuration

You might wonder, what is the lifespan of the instances provided? Do they stay in memory forever like singletons or they are destroyed when the object that has them @Injected is destroyed?

And what is the scope, are all instances for all classes the same, or each class will have a new instance for its @Injected?

The answer is, by default, all the instances are created for each @Injected and are destroyed once the object that holds @Injected is destroyed.

But you can change that with the Scope and Lifespan, default values would be:

@Injected(\.networking, scope: .local, lifespan: .temporary) var network

here are the possible values:

Scope

  • .local - new instance for each @Injected
  • .shared - same instance for all @Injected

Lifespan

  • .permanent - instance stays till the app is deallocated.
  • .temporary - instance deallocated when the last @Injected referencing it is deallocated.

Why yet another DI framework?

This is the question I asked the most.

Here are some of the reasons to try Inject and decide for yourself:

  • Thread safety using @MainActor
  • Inject doesn't resolve instances using a container, it doesn't have a container in the first place. Which is a huge advantage over other popular DI solutions for which it is the biggest bottleneck.
  • Compile-time check that all the instances provided, which removes a whole layer of errors.
  • Inject's API operates simple concepts like instance, injection/replacement, scope, and lifespan.
  • It's extremely modular, you one-line it anywhere you want.

There is much more than that, I will soon provide a couple of examples using inject in the app and in another library to showcase how powerful and flexible Inject is.

Meanwhile, it's a good candidate for you to try and make your dependency injection a breeze.

Installation

Adding the dependency

Inject is designed for Swift 5. To depend on the Inject package, you need to declare your dependency in your Package.swift:

.package(url: "https://github.com/MaximBazarov/Inject.git", from: "1.0.0")
You might also like...
ViperServices - Simple dependency injection container for services written for iOS in swift supporting boot order

ViperServices Introduction ViperServices is dependency injection container for iOS applications written in Swift. It is more lightweight and simple in

Needle - Compile-time safe Swift dependency injection framework
Needle - Compile-time safe Swift dependency injection framework

Needle is a dependency injection (DI) system for Swift. Unlike other DI frameworks, such as Cleanse, Swinject, Needle encourages hierarchical DI struc

CarbonGraph - A Swift dependency injection / lookup framework for iOS

CarbonGraph is a Swift dependency injection / lookup framework for iOS. You can

Toledo - a dependency injection library for Swift that statically generates resolvers at compile-time.

Toledo Toledo is a dependency injection library for Swift that statically generates resolvers at compile-time. Index Features Installation Usage Licen

A new approach to Container-Based Dependency Injection for Swift and SwiftUI.
A new approach to Container-Based Dependency Injection for Swift and SwiftUI.

A new approach to Container-Based Dependency Injection for Swift and SwiftUI. Why do something new? Resolver was my first Dependency Injection system.

Container is a lightweight dependency injection framework for Swift.
Container is a lightweight dependency injection framework for Swift.

Container Container is a lightweight dependency injection framework for Swift. Installation is available in the Swift Package Manager. Swift Package M

Corridor  A Coreader-like Dependency Injection μFramework
Corridor A Coreader-like Dependency Injection μFramework

Corridor A Coreader-like Dependency Injection μFramework Table of Contents Why | Examples | Usage | Installation | Credits & License | Why In order to

Deli is an easy-to-use Dependency Injection Container that creates DI containers
Deli is an easy-to-use Dependency Injection Container that creates DI containers

Deli is an easy-to-use Dependency Injection Container that creates DI containers with all required registrations and corresponding factories.

Dip is a simple Dependency Injection Container.

Dip is a simple Dependency Injection Container. It's aimed to be as simple as possible yet p

Comments
  • Multiple instances of the same permanent and shared property

    Multiple instances of the same permanent and shared property

    Hi, great injection library, really simple to use !

    I'm having an issue with the permanent lifespan and the shared scope.

    All my injected properties in my ViewModels are stored like this:

    @Injected(\.submissions, lifespan: .permanent, scope: .shared) var submissionsUseCases
    @Injected(\.structure, lifespan: .permanent, scope: .shared) var structureUseCases
    ...
    

    Despite this, some of my injected properties are newly instantiated even though there already is a value for the same KeyPath in the permanentStorage (in DefaultValues class)

    Here is a print object of the permanentStorage static property at one point:

    (lldb) po permanentStorage
    ▿ 14 elements
      ▿ 0 : 2 elements
        ▿ key : StorageID
          - storageKeyPath : <KeyPath<DefaultValues, StructureUseCases>: 0x600000eb1940>
          - dependencyKey : nil
        ▿ value : <MockStructureUseCases: 0x600001b7f100>
      ▿ 1 : 2 elements
        ▿ key : StorageID
          - storageKeyPath : <KeyPath<DefaultValues, ViewParamUseCases>: 0x600000eddf40>
          - dependencyKey : nil
        ▿ value : <MockViewParamUseCases: 0x600001be42a0>
      ▿ 2 : 2 elements
        ▿ key : StorageID
          - storageKeyPath : <KeyPath<DefaultValues, ListUseCases>: 0x6000001fc100>
          - dependencyKey : nil
        ▿ value : <MockListUseCases: 0x600001bc7bc0>
      ▿ 3 : 2 elements
        ▿ key : StorageID
          - storageKeyPath : <KeyPath<DefaultValues, SubmissionsUseCases>: 0x600000edfd80>
          - dependencyKey : nil
        ▿ value : <MockSubmissionsUseCases: 0x600001598c00>
      ▿ 4 : 2 elements
        ▿ key : StorageID
          - storageKeyPath : <KeyPath<DefaultValues, AppUseCases>: 0x600000ed6540>
          - dependencyKey : nil
        - value : <MockAppUseCases: 0x60000197c010>
      ▿ 5 : 2 elements
        ▿ key : StorageID
          - storageKeyPath : <KeyPath<DefaultValues, StructureUseCases>: 0x600000e94ec0>
          - dependencyKey : nil
        ▿ value : <MockStructureUseCases: 0x600001bcb320>
      ▿ 6 : 2 elements
        ▿ key : StorageID
          - storageKeyPath : <KeyPath<DefaultValues, UserUseCases>: 0x6000001fd0c0>
          - dependencyKey : nil
        ▿ value : <MockUserUseCases: 0x600001be5ba0>
      ▿ 7 : 2 elements
        ▿ key : StorageID
          - storageKeyPath : <KeyPath<DefaultValues, StructureUseCases>: 0x6000001f2cc0>
          - dependencyKey : nil
        ▿ value : <MockStructureUseCases: 0x600001be6b80>
      ▿ 8 : 2 elements
        ▿ key : StorageID
          - storageKeyPath : <KeyPath<DefaultValues, AccountsUseCases>: 0x600000ed6dc0>
          - dependencyKey : nil
        - value : <MockAccountsUseCases: 0x600001970440>
      ▿ 9 : 2 elements
        ▿ key : StorageID
          - storageKeyPath : <KeyPath<DefaultValues, WorkflowUseCases>: 0x600000e94cc0>
          - dependencyKey : nil
        ▿ value : <MockWorkflowUseCases: 0x600001be5c00>
      ▿ 10 : 2 elements
        ▿ key : StorageID
          - storageKeyPath : <KeyPath<DefaultValues, SearchUseCases>: 0x600000eddf00>
          - dependencyKey : nil
        ▿ value : <MockSearchUseCases: 0x600001598930>
      ▿ 11 : 2 elements
        ▿ key : StorageID
          - storageKeyPath : <KeyPath<DefaultValues, FilesUseCases>: 0x6000001fccc0>
          - dependencyKey : nil
        - value : <MockFilesUseCases: 0x600001970600>
      ▿ 12 : 2 elements
        ▿ key : StorageID
          - storageKeyPath : <KeyPath<DefaultValues, AccountsUseCases>: 0x600000ec6ec0>
          - dependencyKey : nil
        - value : <MockAccountsUseCases: 0x60000197c000>
      ▿ 13 : 2 elements
        ▿ key : StorageID
          - storageKeyPath : <KeyPath<DefaultValues, AppUseCases>: 0x600000ec1580>
          - dependencyKey : nil
        - value : <MockAppUseCases: 0x60000197c020>
    

    As you can see, there are multiple instances of MockStructureUseCases, MockAccountsUseCases... even though they're all permanent and shared (no dependencyKey)

    Do you have any idea why this happens ?

    Thanks

    opened by ValentinDenis 0
Releases(1.0.2)
  • 1.0.2(Oct 18, 2022)

    Dependency.override(_:) is now public so it's possible to override dependency in your production code like _someService.override(SomeService()).

    It's particularly useful if you want to open the specific injections in your tests for something otherwise global.

    E.g you have something that depends on a global storage, and you don't want to make your code passing storage around to maintain it. You can use override in your tests for this injection point specifically.

    Source code(tar.gz)
    Source code(zip)
  • 1.0.1(Oct 13, 2022)

  • 1.0.0(Oct 13, 2022)

Owner
Maxim Bazarov
Maxim Bazarov
DIContainer Swift is an ultra-light dependency injection container made to help developers to handle dependencies easily. It works with Swift 5.1 or above.

?? DIContainer Swift It is an ultra-light dependency injection container made to help developers to handle dependencies easily. We know that handle wi

Victor Carvalho Tavernari 10 Nov 23, 2022
Pilgrim - Dependency injection for Swift (iOS, OSX, Linux). Strongly typed, pure Swift successor to Typhoon.

pilgrim.ph Pilgrim is a dependency injection library for Swift with the following features: Minimal runtime-only library that works with pure Swift (s

AppsQuick.ly 60 Oct 24, 2022
Injector - A Swift package for simple dependency injection that also supports Swift UI previews

A Swift package for simple dependency injection that also supports Swift UI prev

null 6 Aug 9, 2022
Cleanse is a dependency injection framework for Swift.

Cleanse - Swift Dependency Injection Cleanse is a dependency injection framework for Swift. It is designed from the ground-up with developer experienc

Square 1.7k Dec 16, 2022
DIKit Dependency Injection Framework for Swift, inspired by KOIN.

DIKit Dependency Injection Framework for Swift, inspired by KOIN. Basically an implementation of service-locator pattern, living within the applicatio

null 95 Dec 22, 2022
Tranquillity is a lightweight but powerful dependency injection library for swift.

DITranquillity Tranquillity is a lightweight but powerful dependency injection library for swift. The name "Tranquillity" laid the foundation in the b

Ivlev Alexander 393 Dec 24, 2022
Swinject is a lightweight dependency injection framework for Swift.

Swinject Swinject is a lightweight dependency injection framework for Swift. Dependency injection (DI) is a software design pattern that implements In

null 5.6k Dec 31, 2022
Dependency Injection framework for Swift (iOS/macOS/Linux)

Declarative, easy-to-use and safe Dependency Injection framework for Swift (iOS/macOS/Linux) Features Dependency declaration via property wrappers or

Scribd 684 Dec 12, 2022
Swift Ultralight Dependency Injection / Service Locator framework

Swift Ultralight Dependency Injection / Service Locator framework

Michael Long 1.9k Jan 6, 2023
Kraken - Simple Dependency Injection container for Swift. Use protocols to resolve dependencies with easy-to-use syntax!

Kraken Photo courtesy of www.krakenstudios.blogspot.com Introduction Kraken is a simple Dependency Injection Container. It's aimed to be as simple as

Syed Sabir Salman-Al-Musawi 1 Oct 9, 2020