Understanding Dependency Injection in iOS with a Basic Dependency Container
In the world of iOS development, keeping our code clean, maintainable, and testable is crucial. One way to achieve this is through Dependency Injection (DI), a design pattern that might sound complex but is actually quite straightforward. Let’s break it down with a simple example: a class I’ll call BYDependencyContainer.
What is Dependency Injection?
Dependency Injection is a fancy term for a simple idea: instead of your objects creating their own dependencies, something else gives those dependencies to them. This “something else” is often a piece of code dedicated to this task, which can help make your code less tangled and easier to manage.
Why Use DI?
- Testability: It’s easier to test objects when you can control their dependencies.
- Flexibility: You can change parts of your app without rewriting a lot of code.
- Simplicity: Your classes don’t need to know where their dependencies come from.
Introducing BYDependencyContainer
BYDependencyContainer
is a basic DI container. It holds onto the dependencies your app needs, like services or managers, so your app's components don't have to create them. It uses a pattern called "singleton," which means there's only one instance of it in your app.
public class BYDependencyContainer {
// MARK: Properties
public static let shared = BYDependencyContainer()
private var instances = [String: Any]()
// Private initializer to prevent external instantiation
private init() {}
/// Registers an instance of a dependency.
/// The type of the instance is inferred from the instance itself.
/// - Parameter instance: The instance to be registered.
public func register<T>(_ instance: T) {
let key = String(describing: T.self)
instances[key] = instance
}
/// Resolves and returns an instance of the requested dependency type.
/// If the instance has not been registered, this method will trigger a fatal error.
/// - Returns: An instance of the requested type.
public func resolve<T>() -> T {
let key = String(describing: T.self)
guard let instance = instances[key] as? T else {
fatalError("No instance registered for \(key)")
}
return instance
}
}
How It Works?
- Register Dependencies: You tell
BYDependencyContainer
about a dependency by registering it. This is usually done at the start of your app, like in theAppDelegate
. - Resolve Dependencies: When a part of your app needs a dependency, it asks the container for it.
Example in Action
First, we set up our dependencies in the AppDelegate
. It might look something like this:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Registering dependencies
let networkService = NetworkService()
BYDependencyContainer.shared.register(networkService)
let userManager = UserManager(networkService: networkService)
BYDependencyContainer.shared.register(userManager)
return true
}
Now, whenever a part of your app needs one of these dependencies, it can simply ask for it:
class UserProfileViewController: UIViewController {
private var userManager: UserManager!
override func viewDidLoad() {
super.viewDidLoad()
userManager = BYDependencyContainer.shared.resolve()
}
}
In this case, UserProfileViewController
gets UserManager
from the container, keeping it decoupled from the creation logic.
Conclusion
Dependency Injection doesn’t have to be complicated. With a basic understanding and a simple class like BYDependencyContainer
, you can make your iOS projects more manageable and maintainable. This pattern helps in separating concerns, making your code cleaner and your life as a developer a bit easier.