Needle - Compile-time safe Swift dependency injection framework

Overview

Build Status Carthage compatible License

Needle is a dependency injection (DI) system for Swift. Unlike other DI frameworks, such as Cleanse, Swinject, Needle encourages hierarchical DI structure and utilizes code generation to ensure compile-time safety. This allows us to develop our apps and make code changes with confidence. If it compiles, it works. In this aspect, Needle is more similar to Dagger for the JVM.

Needle aims to achieve the following primary goals:

  1. Provide high reliability by ensuring dependency injection code is compile-time safe.
  2. Ensure code generation is highly performant even when used with multi-million-line codebases.
  3. Be compatible with all iOS application architectures, including RIBs, MVx etc.

The gist

Using Needle to write DI code for your application is easy and compile-time safe. Each dependency scope is defined by a Component. And its dependencies are encapsulated in a Swift protocol. The two are linked together using Swift generics.

/// This protocol encapsulates the dependencies acquired from ancestor scopes.
protocol MyDependency: Dependency {
    /// These are objects obtained from ancestor scopes, not newly introduced at this scope.
    var chocolate: Food { get }
    var milk: Food { get }
}

/// This class defines a new dependency scope that can acquire dependencies from ancestor scopes
/// via its dependency protocol, provide new objects on the DI graph by declaring properties,
/// and instantiate child scopes.
class MyComponent: Component<MyDependency> {

    /// A new object, hotChocolate, is added to the dependency graph. Child scope(s) can then
    /// acquire this via their dependency protocol(s).
    var hotChocolate: Drink {
        return HotChocolate(dependency.chocolate, dependency.milk)
    }

    /// A child scope is always instantiated by its parent(s) scope(s).
    var myChildComponent: MyChildComponent {
        return MyChildComponent(parent: self)
    }
}

This is pretty much it, when writing DI code with Needle. As you can see, everything is real, compilable Swift code. No fragile comments or "annotations". To quickly recap, the three key concepts here are dependency protocol, component and instantiation of child component(s). Please refer to the Getting started with Needle section below for more detailed explanations and advanced topics.

Getting started with Needle

Using and integrating with Needle has two steps. Each of the following steps has detailed instructions and explanations in the linked documents.

  1. Integrate Needle's code generator with your Swift project.
  2. Write application DI code following NeedleFoundation's API.

Installation

Needle has two parts, the NeedleFoundation framework and the executable code generator. Both parts need to be integrated with your Swift project in order to use Needle as your DI system.

Install NeedleFoundation framework

Using Carthage

Please follow the standard Carthage installation process to integrate the NeedleFoundation framework with your Swift project.

github "https://github.com/uber/needle.git" ~> VERSION_OF_NEEDLE

Using Swift Package Manager

Please specify Needle as a dependency via the standard Swift Package Manager package definition process to integrate the NeedleFoundation framework with your Swift project.

dependencies: [
    .package(url: "https://github.com/uber/needle.git", .upToNextMajor(from: "VERSION_NUMBER")),
],
targets: [
    .target(
        name: "YOUR_MODULE",
        dependencies: [
            "NeedleFoundation",
        ]),
],

Using CocoaPods

Please follow the standard pod integration process and use NeedleFoundation pod.

Install code generator

Using Carthage

If Carthage is used to integrate the NeedleFoundation framework, then a copy of the code generator executable of the corresponding version is already downloaded in the Carthage folder. It can be found at Carthage/Checkouts/needle/Generator/bin/needle.

Using Homebrew

Regardless of how the NeedleFoundation framework is integrated into your project, the generator can always be installed via Homebrew.

brew install needle

Why use dependency injection?

The linked document uses a somewhat real example to explain what the dependency injection pattern is, and its benefits.

Related projects

If you like Needle, check out other related open source projects from our team:

  • Swift Concurrency: a set of concurrency utility classes used by Uber, inspired by the equivalent java.util.concurrent package classes.
  • Swift Abstract Class: a light-weight library along with an executable that enables compile-time safe abstract class development for Swift projects.
  • Swift Common: common libraries used by this set of Swift open source projects.

License

FOSSA Status

Comments
  • Address usage with RIBs

    Address usage with RIBs

    I see that #3 has disposed of RIBs as something that would unnecessary complicate the tutorial / example project and even obscure the subject matter. Nonetheless, it is my sincere desire to learn more about the best-practice workflows that exist when using Needle to manage RIBs components.

    opened by ermik 16
  • Add printer for dependency graph

    Add printer for dependency graph

    This command line parameter prints out the static dependency tree starting at RootComponent.

    Command: needle generate NeedleGenerated.swift apps/freight/ -print-dep

    Example Output:

    Root
    	LoggedIn
    		Active
    	LoggedOut
    		EntryCore
    
    opened by mzheng 8
  • Upgraded to Xcode 12.5 and Swift 5.4

    Upgraded to Xcode 12.5 and Swift 5.4

    Reason

    Issues with Needle on Xcode 12.5 using SPM for integration.

    Screen Shot 2021-05-06 at 13 19 49

    Changes

    • Updated swift-syntax to 0.50400.0 to fix _InternalSwiftSyntaxParser issue;
    • Bumped Xcode on Travic CI to 12.5.

    Testing

    • Verified manually using SPM install and Xcode 12.5
    opened by RomanTysiachnik 7
  • SwiftSyntax issue with binary from Homebrew

    SwiftSyntax issue with binary from Homebrew

    Hi there!

    I faced with the following issue using 0.16.2 version that I installed via Homebrew.

    The error output looks like: error: 💩 Unknown error: SwiftSyntax parser library isn't compatible

    Seems like there is a missed detail for distribution of the latest version via Homebrew.

    There is no issue if I'm using the binary with the provided version of lib_InternalSwiftSyntaxParser.dylib from the repository.

    opened by morozkin 7
  • Unable to find a specification for NeedleFoundation

    Unable to find a specification for NeedleFoundation

    The CocoaPods Specs repo does not have the most recent NeedleFoundation.podspec (v0.9.1)

    This creates a discrepancy on the autogenerated code since Needle Generator will add a reference to NeedleFoundation but CocoaPods will generate Needle.framework

    opened by arueda 6
  • Provide instructions how to use it

    Provide instructions how to use it

    I would love to give it a try and maybe contribute to this project.

    How am I suppose to use it? Is there some wiki page or should I look at the code?

    Btw, thanks guys, highly appreciated for these efforts.

    opened by pbrazdil 6
  • Proper usage of shared to avoid memory cycles

    Proper usage of shared to avoid memory cycles

    I struggle to wrap my head around memory management in Needle and I'm looking for guidance, or maybe just confirmation that I understand things well.

    Non-Pluginized Components

    class ParentComponent: Component<EmptyDependency> {
        var viewController: ParentVC {
            shared {
                ParentVC(childVCBuilder: childComponent)
            }
        }
    
        var childComponent: ChildComponent {
            ChildComponent(parent: self)
        }
    }
    
    class ParentVC: UIViewController {
        let childVCBuilder: ChildVCBuilding
    
        init(childVCBuilder: ChildVCBuilding) {
            self.childVCBuilder = childVCBuilder
        }
    }
    
    protocol ChildVCBuilding {
        var viewController: ChildVC { get }
    }
    
    class ChildComponent: Component<EmptyDependency>, ChildVCBuilding {
        var viewController: ChildVC {
            ChildVC()
        }
    }
    
    

    I expect this code to create a memory cycle (ParentComponent -> ParentVC -> ChildComponent -> ParentComponent), hence my first question: is it correct to say that a component that has children components should not use shared when creating their associated VC?

    Pluginized Components

    I actually have less problems with pluginized components. I understand that the plugin extension weakly (with unowned) reference the non-core component so the only potential cycle is if a developer doesn't properly bind the pluginized component to the VC.

    Though, I guess it doesn't make much sense to bind a component with multiple VC, hence my second question: is it correct to say that pluginized components must always used shared when creating their associated VC?

    Combining the two

    Combining the two first observations, assuming they are correct, is it correct to say that a PluginizedComponent should not have children components (apart from its non-core component), otherwise we're stuck in a contradiction where the VC both must be wrapped in shared (as a pluginized component) and not wrapped in shared (as a component with children components).

    opened by aurelienp 5
  • Fighting over-usage of

    Fighting over-usage of "dynamic" dependency without introducing infinite loop

    As I start using Needle in my project, I frequently face the dilemna of having to heavily rely on "dynamic" dependency injection in order to avoid infinite loops. As a result, the code feels like it's not fully embracing the Needle approach.

    I think the best way to explain this problem is with the following example. MyChildVC is a child VC of MyVC. The MyChildVC instance is created in MyVC.init (it could probably be done differently but it's off-topic). MyChildVC depends on a property built by MyVC itself.

    I wonder if I'm missing something obvious to solve this problem and/or if there's a recommended approach to workaround this for a codebase that must scale.

    // MARK: Parent component
    
    protocol MyDependency: Dependency {}
    
    class MyComponent: Component<MyDependency>, MyVCBuilder {
        var myVC: MyVC {
            shared {
                MyVC(
                    aStreamInitialValue: 0,
                    aChildVCBuilder: myChildComponent
                )
            }
        }
    
        var myChildComponent: MyChildComponent {
            MyChildComponent(parent: self)
        }
    
        var aStreamInitialValueSetupByParent: Int {
            // Refers to `myVC` before init finishes, leading to and infinite loop
            myVC.aStreamSource
        }
    
        var aStreamSetupByParent: AnyPublisher<Int, Never> {
            // Refers to `myVC` before init finishes, leading to and infinite loop
            myVC.$aStreamSource
        }
    }
    
    protocol MyVCBuilder {
        var myVC: MyVC { get }
    }
    
    class MyVC: UIViewController {
        @Published var aStreamSource: Int
    
        let childVC: MyChildVC
    
        init(
            aStreamInitialValue: Int,
            aChildVCBuilder: MyChildVCBuilder
        ) {
            aStreamSource = aStreamInitialValue
            
            // The "problem" illustrated with this example comes from the facts that:
            // - this builder is called in `MyVC.init` (but even if it wasn't, I'd expect the external code to be defensive)
            // - `aStream`, dependency needed by the child, is setup inside MyVC
            //
            // In a non-Needle world, I would pass `aStreamSource` and `$aStreamSource` directly into the builder here.
            // (eventually relying on "dynamic" dependency if I understand your nomenclature properly).
            // But it doesn't feel very Needle-y. Eventually, it means passing an heavy amount of dependencies "manually",
            // somewhat defeating the Needle system, and it's only to workaround an infinite loop.
            childVC = aChildVCBuilder.myChildVC
    
            super.init(nibName: nil, bundle: nil)
        }
    }
    
    // MARK: Child component
    
    protocol MyChildDependency: Dependency {
        var aStreamInitialValueSetupByParent: Int
        var aStreamSetupByParent: AnyPublisher<Int, Never>
    }
    
    class MyChildComponent: MyChildVCBuilder {
        var myChildVC: MyChildVC {
            MyChildVC(
                aStreamInitialValue: dependency.aStreamInitialValueSetupByParent
                aStream: dependency.aStreamSetupByParent
            )
        }
    }
    
    protocol MyChildVCBuilder {
        var myChildVC: MyChildVC { get }
    }
    
    class MyChildVC: UIViewController {
        let aStreamInitialValue: Int
        let aStream: AnyPublisher<Int, Never>
    
        init(aStreamInitialValue: Int, aStream: aStream) {
            self.aStreamInitialValue = aStreamInitialValue
            self.aStream = aStream
    
            super.init(nibName: nil, bundle: nil)
        }
    }
    

    EDIT: I corrected the aStreamSetupByParent type issue that you mentioned in your first comment @rudro. Thanks for pointing that out.

    opened by aurelienp 5
  • Rename Needle podspec to NeedleFoundation

    Rename Needle podspec to NeedleFoundation

    Needle generator expects framework with name NeedleFoundation to exist. Current podspec has name Needle therefore code fails to build since there is no such pod with name NeedleFoundation.

    opened by iprox 5
  • Library not loaded: @rpath/lib_InternalSwiftSyntaxParser.dylib

    Library not loaded: @rpath/lib_InternalSwiftSyntaxParser.dylib

    When trying to run needle's latest release 0.16.0 we see an error that it can't find the SwiftSyntaxParser.dylib. The same was happening with mockolo at some point and mockolo is shipping the dylib with its release. The solution is probably to ship the dylib with the releases inside the Generator/bin folder. Or do you have another solution in mind?

    We are looking forward to using the SwiftSyntaxParser implementation but this currently blocks updating for us.

    Mockolo issue: https://github.com/uber/mockolo/issues/108

    Bug 
    opened by FranzBusch 4
  • Add dynamicMemberLookup to Component for direct dependency access

    Add dynamicMemberLookup to Component for direct dependency access

    Needle codebases will end up with dependencies coming from both the component and the component's dependency protocol:

    let interactor = LoggedInInteractor(loggedInStream: component.dependency.loggedInStream,
                                        networkClient: component.networkClient,
                                        appUpdater: component.dependency.appUpdater,
                                        modelProvider: component.modelProvider,
                                        modelRefresher: component.dependency.modelRefresher)
    

    We can leverage Swift 5.1's dynamicMemberLookup keypath support to make property access consistent for all component dependencies and avoid developers the slight pain of typing component. or component.dependency. to find their dependency, and unnecessary updates during dependency refactor.

    let interactor = LoggedInInteractor(loggedInStream: component.loggedInStream,
                                        networkClient: component.networkClient,
                                        appUpdater: component.appUpdater,
                                        modelProvider: component.modelProvider,
                                        modelRefresher: component.modelRefresher)
    
    opened by ebgraham 4
  • Very high compile times in release mode for our largest app

    Very high compile times in release mode for our largest app

    It's unclear if this issue has been around for a while or it's new in Swift 5.7/Xcode 14. We only just noticed this issue.

    For our latest app, the Uber rides app, the needle generated code take over an hour to compile in Release (i.e. -Osize) mode. The issue is demonstrated in a toy project and reported here : https://github.com/apple/swift/issues/62124

    The workaround for this is to add a @inline(never) to some of the functions. Until the compiler performance issue is fixed, we should use this workaround in the needle generated code.

    opened by rudro 0
  • How to resolve dependency cycle?

    How to resolve dependency cycle?

    I am going through migrating a legacy codebase onto using Needle and encountered circular dependencies. Some of my scopes have children scopes that in turn declare one of the great great parent components as their dependency, hence the dependency cycle.

    The scenario I'm dealing with isn't necessarily an error - the UI I have is for users browsing through folders which means the user can open one folder and then another subfolder and then another infinitively showing the same UI/component as a child of the previous one. Think dropbox file browsing.

    The issue I'm experiencing won't be a problem at runtime but since Needle is trying to resolve the dependency tree before compile time so that it can be deterministic and correct at compile time it can't really process it.

    How can this be resolved? Some sort of a "break" at some point with BootstrapComponent creating a second parallel dependency tree for the part of the app I want to circularly depend on?

    opened by alexvbush 2
  • Getting double results

    Getting double results

    Seems like I'm getting double results when Needle identifies missing dependencies.

    ios % needle generate upkeep/NeedleGenerated.swift Source
    warning: ❗️ Could not find a provider for (userDefaults: UserDefaults) which was required by NotificationHubMenuDependency, along the DI branch of ^->AppDependencies->NotificationHubComponent->NotificationHubMenuComponent.
    warning: ❗️ Could not find a provider for (userDefaults: UserDefaults) which was required by NotificationHubMenuDependency, along the DI branch of ^->AppDependencies->NotificationHubComponent->NotificationHubMenuComponent.
    warning: ❗️ Missing one or more dependencies at scope.
    error: 💩 Some dependencies are missing, please look at the warnings above for the list.
    

    Is this expected or am I configuring something wrong?

    opened by alexvbush 0
  • Generated Code doesn't work with package specification

    Generated Code doesn't work with package specification

    I have a multi-module configuration, but if I specify a package with RootComponent, NeedleGenerated is not generated correctly.

    I'll show you an example below. If you remove the HogeModule designation, it works fine.

    Example

    import HogeModule
    final class RootComponent: BootstrapComponent {
        var hogeComponent: HogeBuilder {
            HogeModule.HogeComponent(parent: self)
        }
    }
    
    public func registerProviderFactories() {
        __DependencyProviderRegistry.instance.registerDependencyProviderFactory(for: "^->RootComponent") { component in
            return EmptyDependencyProvider(component: component)
        }   
    }
    
    opened by satoshi-baba-0823 0
  • Needle should generate a alternate version of the generated code that's more runtime reliant

    Needle should generate a alternate version of the generated code that's more runtime reliant

    The total volume of code needle produces is O(average_number_of_items_in_the_dependency_protocols x total_number_of_paths_in_the_component_tree). At Uber, the first one is slightly high maybe, but the second part is really high in our flagship apps. This is due to the total number of unique components being high and some nodes being repeated in many places.

    Instead of such explicit code that we can rely on the Swift compiler to perform a second layer of safety checks for us with, we could create dictionaries at each component. Then we could rely on dynamic member lookup and then, at runtime, walk up the tree of components (to ancestors) and check on these dictionaries and check if the dictionary has the item we are looking for.

    opened by rudro 1
Releases(v0.22.0)
Owner
Uber Open Source
Open Source Software at Uber
Uber Open Source
Injection - Dependency injection using property wrappers

Dependency injection using property wrappers. Registering types: // injecting a

Alejandro Ramirez 3 Mar 14, 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
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
CarbonGraph - A Swift dependency injection / lookup framework for iOS

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

Baidu 244 Jan 4, 2023
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

Aleksei Artemev 17 Oct 13, 2022
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
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
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
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

Siarhei Ladzeika 5 Dec 8, 2022
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.

Michael Long 573 Jan 2, 2023
Effortless modular dependency injection for Swift.

Inject Effortless modular dependency injection for Swift. Sometimes during the app development process we need to replace instances of classes or acto

Maxim Bazarov 40 Dec 6, 2022
Guise - An elegant, flexible, type-safe dependency resolution framework for Swift

Guise is an elegant, flexible, type-safe dependency resolution framework for Swift. Flexible dependency resolution, with optional caching Elegant, str

null 52 Oct 3, 2022
An elegant, flexible, type-safe dependency resolution framework for Swift

Guise is an elegant, flexible, type-safe dependency resolution framework for Swift. Flexible dependency resolution, with optional caching Elegant, str

null 52 Oct 3, 2022
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

symentis GmbH 60 Nov 1, 2022