The Composition Root in UIKit Apps
In UIKit applications, the Composition Root emerges as a fundamental concept, serving as the cornerstone for building modular, maintainable, and testable applications. This blog post aims to unravel the significance of the Composition Root, its key principles, and how it contributes to crafting robust software architectures.
This article focuses on UIKit because the examples use
SceneDelegate,UIViewController, factories, and flows. If you are building a pure SwiftUI app, read The Composition Root in SwiftUI Apps next. The principle is the same, but the entry point and ownership rules look different.
#Understanding the Composition Root
The Composition Root is the central place in your application where the dependency graph is constructed. It's the point where various components, services, and dependencies are brought together and injected into the application's core. By encapsulating the configuration logic in the Composition Root, we achieve a high degree of flexibility, making the system easily adaptable to changes.
The Composition Root is a Layer, not a single Class or a single Method. It's simply a term that refers to the place where the application's dependencies are composed and wired together. This could be Factories, Builders, etc.
// Example: Composition Root in UIKit
class SceneDelegate {
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let window = UIWindow(windowScene: scene as! UIWindowScene)
window.rootViewController = CompositionRoot().createRootViewController()
window.makeKeyAndVisible()
}
}
class CompositionRoot {
let navigationController = UINavigationController()
func createRootViewController() -> UIViewController {
let dataFetcher = RemoteHomeFetcher()
let apiService = ApiService(dataFetcher: dataFetcher)
let homeViewModel = HomeViewModel(apiService: apiService)
let homeViewController = HomeViewController(viewModel: homeViewModel)
let productFlow = createProductFlow()
homeViewController.onProductSelected = productFlow.start
navigationController.viewControllers = [homeViewController]
return navigationController
}
private func createProductFlow() -> ProductFlow {
return ProductFlow(
navigationController: navigationController,
createProductViewController: {
let dataFetcher = RemoteProductFetcher()
let apiService = ProductApiService(dataFetcher: dataFetcher)
let productViewModel = ProductViewModel(apiService: apiService)
return ProductViewController(viewModel: productViewModel)
}
)
}
class ProductFlow {
let navigationController: UINavigationController
let createProductViewController: () -> UIViewController
init(
navigationController: UINavigationController,
createProductViewController: @escaping () -> UIViewController
) {
self.navigationController = navigationController
self.createProductViewController = createProductViewController
}
func start() {
navigationController.pushViewController(
createProductViewController(),
animated: true
)
}
}
}
#Advantages of a Well-Defined Composition Root
Testability
The Composition Root greatly facilitates unit testing by allowing easy substitution of dependencies with mocks or stubs. This ensures that each component can be tested in isolation, promoting a robust testing suite.
Maintainability
Centralizing the configuration logic in the Composition Root enhances the maintainability of the codebase. When changes are required, developers can focus on this specific area rather than scattering modifications throughout the entire application.
Flexibility and Adaptability
As the application evolves, the Composition Root's encapsulation of dependencies provides a flexible architecture. Introducing new features or modifying existing ones becomes a streamlined process, as the changes are confined to the Composition Root.
#Best Practices
Keep it Simple
The Composition Root should be concise and focused on configuring dependencies. Avoid unnecessary complexity and keep the logic straightforward.
Use Dependency Injection
Embrace the principle of dependency injection to ensure that components receive their dependencies from external sources, making the system more modular and decoupled.
In conclusion, understanding and implementing a well-structured Composition Root is paramount for building scalable and maintainable UIKit applications. By adhering to the principles outlined here, developers can harness the power of a solid Composition Root to navigate the intricacies of dependency management successfully.
#Adapting to Real-world Scenarios
Dynamic Dependency Configuration
In scenarios where dependencies need dynamic configuration, the Composition Root adapts gracefully. Consider a situation where the base URL of an API is dynamic and determined at runtime, based on an A/B test. The Composition Root can handle such dynamic configurations seamlessly.
class ApiServiceFactory {
let configurationService: ConfigurationService
func makeApiService() -> ApiService {
let dataFetcher = RemoteDataFetcher()
let newDataFetcher = NewRemoteDataFetcher()
let apiService = ApiService(
dataFetcher: configurationService.isNewApiEnabled ? newDataFetcher : dataFetcher
)
return apiService
}
}
// Example usage:
let productsApiService = ApiServiceFactory(configurationService: configurationService).makeApiService()
let productsViewModel = ProductsViewModel(apiService: productsApiService)
Integrating Third-party Libraries
When integrating third-party libraries into an iOS project, the Composition Root acts as the integration point. This ensures that the third-party dependencies are properly configured and seamlessly integrated into the application. If you add dependency injection to the mix, you can easily swap out third-party libraries with minimal changes to the rest of the application.
enum ProductsStorageStrategy {
case userDefaults
case coreData
case realm
}
class ProductsStorageFactory {
func makeProductsStorage(strategy: ProductsStorageStrategy) -> ProductsStorage {
switch strategy {
case .userDefaults:
return UserDefaultsProductsStorage(userDefaults: .standard)
case .coreData:
return CoreDataProductsStorage(coreDataStack: coreDataStack)
case .realm:
return RealmProductsStorage(realm: realm)
}
}
}
// Example usage:
let productsStorage = ProductsStorageFactory()
.makeProductsStorage(strategy: .userDefaults)
let saveProductUseCase = SaveProductUseCase(productsStorage: productsStorage)
#Scaffolding the Composition Root and your Application
Remember, the Composition Root is not a single class or method; it's a layer in your application. It's a place where you wire up your dependencies, and it can be composed of multiple classes and methods. Here's a high-level overview of how you can structure your Composition Root:
├── Application
│ ├── SceneDelegate.swift
├── CompositionRoot
│ ├── RootViewControllerFactory.swift
│ ├── RemoteHomeFetcher.swift (implementation)
│ ├── RemoteProductsFetcher.swift (implementation)
│ ├── ProductsStorageFactory.swift
| ├── RealmProductsStorage.swift (implementation)
├── Features
│ ├── Home
│ │ ├── HomeViewController.swift
│ │ ├── HomeViewModel.swift
│ │ ├── HomeDataFetcher.swift (possibly a protocol)
│ ├── Products
│ │ ├── ProductViewController.swift
│ │ ├── ProductViewModel.swift
│ │ ├── ProductDataFetcher.swift (possibly a protocol)
│ │ ├── ProductStorage.swift (possibly a protocol)
#Conclusion
In the dynamic landscape of UIKit development, the Composition Root stands as a pivotal concept, guiding developers in crafting modular, maintainable, and adaptable applications. By embracing the principles outlined in this exploration, developers can leverage the power of a well-defined Composition Root to navigate the complexities of dependency management with finesse. From dynamic configurations to third-party integrations, the Composition Root remains a stalwart ally in the pursuit of code excellence.
#What you should remember
The composition root is not one magical class. It is the layer where you make application decisions: which implementations exist, how they are wired, and where third-party details enter the system.
Read next
The Flow Pattern in UIKit: Testable Navigation Outside View Controllers
Learn the Flow pattern in UIKit and how it keeps navigation reusable, testable, and separate from view controllers.