DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workkloads.

Secure your stack and shape the future! Help dev teams across the globe navigate their software supply chain security challenges.

Releasing software shouldn't be stressful or risky. Learn how to leverage progressive delivery techniques to ensure safer deployments.

Avoid machine learning mistakes and boost model performance! Discover key ML patterns, anti-patterns, data strategies, and more.

Related

  • SwiftData Dependency Injection in SwiftUI Application
  • Why I Started Using Dependency Injection in Python
  • On SBOMs, BitBucket, and OWASP Dependency Track
  • A Developer’s Guide to Multithreading and Swift Concurrency

Trending

  • Java Virtual Threads and Scaling
  • Unlocking AI Coding Assistants Part 2: Generating Code
  • Java's Quiet Revolution: Thriving in the Serverless Kubernetes Era
  • AI, ML, and Data Science: Shaping the Future of Automation
  1. DZone
  2. Software Design and Architecture
  3. Security
  4. Native Dependency Injection in Swift

Native Dependency Injection in Swift

In this article, we are going to go through a couple of solutions to a common problem when developing mobile applications, and this is Dependency Injection.

By 
Felipe Ferrari user avatar
Felipe Ferrari
·
Nov. 16, 22 · Tutorial
Likes (1)
Comment
Save
Tweet
Share
5.4K Views

Join the DZone community and get the full member experience.

Join For Free

In this article, we are going to go through a couple of solutions to a common problem when developing mobile applications, and this is Dependency Injection.

We all saw a project that was calling singleton classes from any part of the code without any security when writing data to them, battling with race conditions, and coupling implementations to all these issues. You can also find many third-party libraries that can help to solve or at least manage these problems, but many times we don’t want or we can’t add extra dependencies; in this case, a good approach is to implement the solution that fits your needs in a basic but sufficient way and with native code.
To start with this, we will go through the problem first and see what would actually help us. Let’s say we have a class called AViewModel that needs to use ServiceA; the first thing to think of is to instantiate the service in the initializer or directly inside the class like this:

 
class AViewModel {
    let serviceA = ServiceA.shared
}


This does not give us any guarantee that at the moment we call the service, the instance has not been used by some other code that could imply race conditions or unwanted behaviors if not handled. It’s also worth mentioning that this class would be impossible to test decoupled from the ServiceA singleton instance. To improve this a little, we could go with something like this: 

 
protocol ServiceAProtocol {
    func execute()
}

class AViewModel {
    let serviceA = ServiceAProtocol
    init(serviceA: ServiceAProtocol) {
        self.serviceA = ServiceA
    }
}


Now we can create a mocked class that implements ServiceAProtocol, and we can test AViewModel without depending on the ServiceA.shared instance.

To solve this, we can first implement this little system with protocols and typealiases that will let us define which dependencies we need for a certain class and contain that definition in it. If we adapt our AViewModel to handle this, we would have something like this:

 
protocol ServiceAContainer {
    var serviceA: ServiceAProtocol { get }
}

class AViewModel {
    typealias Dependencies = ServiceAContainer
    let dependencies: Dependencies
    init(dependencies: Dependencies) {
        self.dependencies = dependencies
    }
}


Inside AViewModel now, we can access serviceA directly from our dependencies property.

Maybe you still don’t see the benefit of this abstraction, but you will in a bit. For now, let’s see how we could use this implementation with the service as it was before; this could be done so:

 
extension AViewModel {
    struct ADependencies: Dependencies {
        let serviceA: ServiceAProtocol = ServiceA.shared
    }

    convenience init() {
        self.init(dependencies: ADependencies()) 
    }
}


So at this point, if we handle when AViewModel is instantiated, and we are sure it would be ok if it instantiates ServiceA after any other class might have instantiated it, we are good to go.
Imagine that we don’t only have ServiceA but also ServiceB and probably many more that will be used all over our app, and remember that in the last paragraph, we made the assumption that we are not running against any race condition because of using shared instances. If we want to have control of all of these classes, we need to centralize them in some container that we are going to call DependencyContainer.

 
protocol ServiceBContainer {
    var serviceB: ServiceBProtocol { get }
}


 
typealias DependencyContainers = ServiceAContainer
    & ServiceBContainer

struct DependencyContainer: DependencyContainers {
    var serviceA: ServiceAProtocol
    var serviceB: ServiceBProtocol
}


With this little code, we can have a struct that has a reference to specific implementations of the desired services only knowing them by the protocol, and the best of this is that DependencyContainer conforms to a typeAlias that guarantees us that the class has those references.

At this point, we have the possibility to create an instance of this container and access its services like so:

 
let container = DependencyContainer(serviceA: ServiceA.shared, 
                                    serviceB: ServiceB.shared)


And as DependencyContainer conforms to ServiceAContainer because it’s defined in DependencyContainers, we can go ahead and try to adapt our AViewModel to make use of this:

 
extension AViewModel {
    convenience init() {
        let container = DependencyContainer(serviceA: ServiceA.shared, 
                                            serviceB: ServiceB.shared)
        self.init(dependencies: container)    
    }
}


So if we initialized the container at the beginning of the app, we are sure all our instances will be ready when any other class needs them after the app is launched. For example, if we now want to create a new class that uses ServiceB, it would be really straightforward:

 
class BViewModel {
    typealias Dependencies = ServiceBContainer
    let dependencies: Dependencies
    init(dependencies: Dependencies) {
        self.dependencies = dependencies
    }
}

extension BViewModel {
    convenience init() {
        let container = DependencyContainer(serviceA: ServiceA.shared, 
                                            serviceB: ServiceB.shared)
        self.init(dependencies: container)    
    }
}


And if we want to have more than one dependency, it’s as easy as adding them to the specific typealias for each class, and as long as the main container implements this container, you are ready to go.

This implementation will serve the purpose of having a solid DI system without the need for external code, and it’s simple enough to have control of it, but let’s give it a little twist more to make use of property wrappers to simplify a bit the boilerplate code we need to implement each dependency injection.

First, we need to know what a WritableKeyPath is in Swift; in short, Key Paths are expressions to access properties dynamically. In the case of WritableKeyPath, those let you read and write from the value you are accessing, so if we want a property wrapper that provides us a dependency that is inside a container, it means we will have a value stored in DependencyContainer that we can dynamically access with a given WritableKeyPath. Once we make it, it will look like this in our AViewModel:

 
@Dependency(\.aService) var aService


As you can see, there is no need to add any other property nor initialize anything at the point of the integration, which is very convenient. But let’s see the code involved to get to this result, and for this, we need to implement a generic type to store the dependencies in the container; we also need to handle the nil case so we can set dependencies after the container is created. So let’s go with this:

 
/// A protocol to define an injected dependency.
public protocol DependencyKey {
    /// Representing the type of the injected dependency.
    associatedtype Value

    /// The current value of the injected dependency.
    static var currentValue: Self.Value { get set }
}

/// A protocol to define an injected dependency whose initial value is set lazy.
public protocol LazyDependencyKey {
    /// Representing the type of the injected dependency.
    associatedtype Value

    /// The current value of the injected dependency.
    static var currentValue: Self.Value? { get set }
}

extension LazyDependencyKey {
    /// The unwrapped value of the injected dependency. Fails if the actual value has not been set before access.
    static var value: Self.Value {
        get {
            guard let currentValue = currentValue else {
                preconditionFailure("A value must be set before accessing the property.")
            }

            return currentValue
        }
        set {
            currentValue = newValue
        }
    }
}


These protocols have a Value type associated and a currentValue that is an instance of that Value type. With this in place, we can now define our DependencyContainer class as follows:

 
public class DependencyContainer {
    /// Singleton instance used to be accessed by the computed properties.
    private static var current = DependencyContainer()

    /// Access the dependency with the specified `key`.
    /// - Parameter key: Implementation type of `DependencyKey`.
    public static subscript<K>(key: K.Type) -> K.Value where K: DependencyKey {
        get { key.currentValue }
        set { key.currentValue = newValue }
    }

    /// Accesses the dependency with the specified `keyPath`.
    /// - Parameter keyPath: The key path of the computed property.
    public static subscript<T>(_ keyPath: WritableKeyPath<DependencyContainer, T>) -> T {
        get { current[keyPath: keyPath] }
        set { current[keyPath: keyPath] = newValue }
    }

    /// Set the initial value for the specified `key`. This method has to be called before the property is injected or accessed anywhere.
    /// - Parameter initialValue: The initial value that is injected wherever it is used.
    /// - Parameter key: The key to set the value for.
    public static func set<K>(initialValue: K.Value, key: K.Type) where K: LazyDependencyKey {
        key.currentValue = initialValue
    }
}


Here we can see that the container is only storing a value or returning it for a specific Key Path, and that is more or less it. Now we have to define the property wrapper and include it in our original class.
The property wrapper would look something like this:

 
/// A property wrapper type that reflects a dependency injected using `DependencyContainer`.
@propertyWrapper
public struct Dependency<T> {
    private let keyPath: WritableKeyPath<DependencyContainer, T>

    public var wrappedValue: T {
        get { DependencyContainer[keyPath] }
        set { DependencyContainer[keyPath] = newValue }
    }

    public init(_ keyPath: WritableKeyPath<DependencyContainer, T>) {
        self.keyPath = keyPath
    }
}


Now, if for example, we want to use BService in BViewModel, we should first create the key for the service and set it in the container; we also need to define a property to access it directly in the DependencyContainer.

 
public struct BServiceDependencyKey: LazyDependencyKey {
    public static var currentValue:BService?
}

extension DependencyContainer {
    public var bService: BService {
        get { Self[BServiceDependencyKey.self] }
        set { Self[BServiceDependencyKey.self] = newValue }
    }
}


 
...
// Somewhere on your app launch code
    setupDependencies()
}
    
// MARK: - Dependencies
private extension AppDelegate {
    func setupDependencies() {
        DependencyContainer.set(initialValue: BService.shared, key: BServiceDependencyKey.self)
        DependencyContainer.set(initialValue: AService.shared, key: AServiceDependencyKey.self)
    }
}


 
class BViewModel {
    @Dependency(\.bService) var bService
}

class AViewModel {
    @Dependency(\.aService) var aService
}


And that is it! We have a one-line dependency injection with total control of the instances we have at the moment of launching the app and instantiating any class that needs them.

Container Data structure Dependency injection IT Dependency Injection mobile app Property (programming) Protocol (object-oriented programming) Swift (programming language)

Published at DZone with permission of Felipe Ferrari. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • SwiftData Dependency Injection in SwiftUI Application
  • Why I Started Using Dependency Injection in Python
  • On SBOMs, BitBucket, and OWASP Dependency Track
  • A Developer’s Guide to Multithreading and Swift Concurrency

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!