Efficient Dependency Management in iOS: Introducing AppContainer for MVP+DI architecture

Mihail Salari
5 min readAug 14, 2023

--

Dependency Injection

The Essence of Dependency Injection

Imagine you’re constructing a house. Each room depends on specific items: the kitchen needs an oven, the living room needs a television, and the bathroom requires a shower. You wouldn’t want to walk into another room to fetch these items. Instead, you’d expect everything to be in its rightful place, readily available when needed. In the world of iOS development, we deal with a similar concept: Dependency Injection (DI). By ensuring that every component, or ‘room,’ has its required dependencies, or ‘items,’ we can create a more modular and maintainable architecture.

Our Dependencies

Before diving deeper, let’s contextualize our discussion. Suppose we have three dependencies in our application:

  • NetworkManager: Responsible for handling network requests.
  • MoviesService: Fetches movie-related data. This service requires the NetworkManager to fetch data from a remote server.
  • ImageDownloaderManager: Downloads images. It also relies on the NetworkManager to fetch image data.
// NetworkManager
import Foundation

protocol NetworkManagerProtocol { }

final class NetworkManager: NetworkManagerProtocol { }
// MoviesService
import Foundation

protocol MoviesServiceProtocol { }

final class MoviesService: MoviesServiceProtocol {
init(with networkManager: NetworkManagerProtocol) { }
}
// ImageDownloaderManager
import Foundation

protocol ImageDownloaderManagerProtocol { }

final class ImageDownloaderManager: ImageDownloaderManagerProtocol {
init(with networkManager: NetworkManagerProtocol) { }
}

With our dependencies defined, the question becomes: how do we ensure that these components get the services they require? How can we ‘inject’ NetworkManager into both MoviesService and ImageDownloaderManager?

Building upon the MVP+DI architecture we discussed in a previous article, let’s delve into the intricacies of the AppContainer. This article seeks to provide an in-depth exploration of how AppContainer operates as the core DI component, efficiently managing your app's dependencies.

Setting Up Swinject

Before we explore the power of AppContainer, it's crucial to integrate Swinject into our project. Swinject is a lightweight Dependency Injection framework for Swift. It allows us to register and resolve dependencies, making our project more modular and manageable.

Adding Swinject via CocoaPods:

If you are using CocoaPods, you can integrate Swinject by adding the following line to your Podfile:

pod 'Swinject'

Then, run pod install in your terminal.

Adding Swinject via Swift Package Manager (SPM):

  1. In Xcode, navigate to File > Swift Packages > Add Package Dependency.
  2. Enter https://github.com/Swinject/Swinject.git into the search bar and click Next.
  3. Choose the version you’d like to use, then click Next.
  4. Select the target where you want to use Swinject and click Finish.

With Swinject added to our project, we can now harness its power to manage our dependencies seamlessly.

1. What is AppContainer?

AppContainer is a structure that holds the core dependencies your app will frequently use. It's an instance of Swinject's Container class, designed to facilitate Dependency Injection (DI) by providing a centralized registry for services and their dependencies.

Here’s the AppContainer :

import Foundation
import Swinject

struct AppContainer {
static let `default`: Container = {
let container = Container()

// Register your services and dependencies here
container.register(NetworkManagerProtocol.self) { _ in
NetworkManager()
}

container.register(ImageDownloaderManagerProtocol.self) { c in
let network = c.resolve(NetworkManagerProtocol.self)!
return ImageDownloaderManager(with: network)
}

container.register(MoviesServiceProtocol.self) { c in
let network = c.resolve(NetworkManagerProtocol.self)!
return MoviesService(with: network)
}

return Container(parent: container)
}()
}

2. The Need for Dependency Injection

For newcomers, Dependency Injection might sound like an over-engineered solution. However, the benefits are multifold:

  • Decoupling: Separate out the different parts of your application to make it modular.
  • Testability: Replace real implementations with mock ones for unit tests.
  • Scalability: Easily switch out one implementation for another, especially useful as your app grows.

3. Registering Dependencies in AppContainer

Looking at the code, the pattern is clear. Each container.register call links an interface (protocol) to its concrete implementation. This makes it easy to swap out dependencies without affecting dependent components.

container.register(NetworkManagerProtocol.self) { _ in
NetworkManager()
}

Here, any component asking for a NetworkManagerProtocol will receive a NetworkManager instance.

4. Resolving Dependencies

The real magic happens when you need to construct an object that has dependencies. Swinject takes care of that for you:

container.register(ImageDownloaderManagerProtocol.self) { c in
let network = c.resolve(NetworkManagerProtocol.self)!
return ImageDownloaderManager(with: network)
}

In the above code, ImageDownloaderManager requires a NetworkManager. Swinject automatically resolves this dependency and injects it.

5. Using AppContainer in MVP+DI Architecture

With our AppContainer in place, it becomes straightforward to integrate it into our MVP+DI architecture. Presenters, for instance, can directly pull required services from the AppContainer, ensuring that the View layer remains devoid of any logic other than UI-related tasks.
4. Builder Pattern and Dependency Injection

Taking a look at the SplashBuilder, we witness the integration of the AppContainer into the broader architecture:

final class SplashBuilder: SplashBuilderProtocol {
let container = AppContainer.default
...
}

The builder pattern, in harmony with our container, allows modules (like Splash) to be easily instantiated with all their dependencies injected. It’s a testament to the flexibility and power of combining a centralized DI container with the builder pattern.

6. The Bigger Picture: MVP+DI

Incorporating the AppContainer into the MVP+DI structure ensures that Presenters, or any other component, can directly and effortlessly retrieve the services they require. The architecture remains decoupled, and every component knows precisely where to get its dependencies without the need for excessive boilerplate.

Conclusion

The AppContainer complements the MVP+DI architecture, ensuring that your dependencies are managed efficiently and are easily accessible throughout the app. While it's just a piece of the puzzle, it plays a crucial role in ensuring that the architecture remains scalable, testable, and modular.

Remember, architectures and patterns evolve based on needs. What’s essential is to find a balance that caters to the unique requirements of your app.

If you’re eager to delve deeper into these challenges and equip yourself with comprehensive strategies to tackle them, consider investing in a resource that compiles iOS interview insights. 📘 “Cracking the iOS Interview” is one such gem, providing knowledge, confidence, and insights to conquer any iOS development challenge and interview.

📚 Interested? Dive into the world of iOS expertise and grab your copy here. Happy coding! 🎉👨‍💻👩‍💻

🔔 Stay on the cutting edge of the decentralized future by keeping up with articles like this. Connect with me for insider tips on LinkedIn. The future is collaborative, decentralized, and incredibly exciting! 🚀🌍

--

--

Mihail Salari

📱🏆 Senior iOS Engineer / Tech Lead - Made Big Apps for Banks, Crypto and Corporations 🚀 - "Turning Ideas into Apps, and Apps into Success 📱"