Better Programming

Advice for programmers.

Managing Navigation Between Multiple Modules in an iOS App Using Dependency Injection Containers

--

Photo by Deva Darshan on Unsplash

In modern iOS app development, breaking up an application’s functionality into different modules or screens is common. This modular approach allows for better separation of concerns and makes it easier to manage the complexity of large codebases.

However, navigating between modules in a way that maintains code maintainability and reduces coupling can be challenging.

One approach to managing module navigation is using the Dependency Injection design pattern.

Please take a look at my previous articles before continuing. Dependency injection fundamentals are described in the following articles:

Dependency Injection is a software design pattern that separates the creation of objects from their use. In the context of an iOS app, this means decoupling the navigation logic from the individual modules or screens.

By using Dependency Injection, we can ensure that the navigation logic is flexible and easy to modify without modifying the individual modules. The Navigation Controller or Coordinator can be responsible for maintaining the state of the navigation stack and passing dependencies to the destination view controllers.

To illustrate the benefits of using Dependency Injection for navigation, let’s consider an example scenario. Suppose we have an iOS app with three modules: a Login module, a Basket module, and a Payment module.

The Login module is responsible for authenticating the user, while the Basket and Payment modules allow the user to view and modify their basket and payment settings, respectively. We want to ensure the modules are decoupled from one another while also maintaining a clear and intuitive navigation flow for the user.

To achieve this, we can use a Navigation service that is responsible for handling the navigation logic between the modules. The navigation service can be responsible for maintaining the state of the navigation stack and passing dependencies to the destination view controllers.

For example, when the user logs in, the Login module can pass the user’s authentication token to the navigation service, which can then use it to initialize the Basket and Payment modules. The navigation service can also pass other dependencies, such as a network client or database object, to the modules as needed.

By using Dependency Injection for navigation, we can ensure that each module is responsible for its own functionality and does not have to worry about the details of how to navigate to other parts of the app. This reduces coupling and makes it easier to test and maintain the codebase.

Let’s Code

First, let’s define the modules of our app. For this example, we’ll create four modules: a LoginModule, a BasketModule, PaymentModule, and a CoreModule.

I’m only going to cover the critical code examples in this article, and you’ll be able to see the full codebase at the end.

The CoreModule is responsible for the core features, such as DI Container tools, extensions, and utils.

typealias Factory = (DIContainerService) -> Any

protocol ServiceEntryProtocol: AnyObject{
var factory: Factory { get }
var instance: Any? { get set }
}

final public class ServiceEntry: ServiceEntryProtocol {
var instance: Any?

var factory: Factory
weak var container: DIContainer?

init(factory: @escaping Factory) {
self.factory = factory
}
}

public protocol DIContainerService {
func register<Service>(type: Service.Type, name: String?, factory: @escaping (DIContainerService) -> Service) -> ServiceEntry
func resolve<Service>(type: Service.Type, name: String?) -> Service?
}

extension DIContainerService {

@discardableResult
public func register<Service>(type: Service.Type, name: String? = nil, factory: @escaping (DIContainerService) -> Service) -> ServiceEntry {
register(type: type, name: name, factory: factory)
}
public func resolve<Service>(type: Service.Type, name: String? = nil) -> Service? {
resolve(type: type, name: name)
}
}


final public class DIContainer: DIContainerService {
private var services: [String: ServiceEntryProtocol] = [:]
public init() {}

@discardableResult
public func register<Service>(type: Service.Type, name: String?, factory: @escaping (DIContainerService) -> Service) -> ServiceEntry {
let entry = ServiceEntry(factory: factory)
entry.container = self
let key = "\(type)\(name ?? "")"
services[key] = entry
return entry
}

public func resolve<Service>(type: Service.Type, name: String?) -> Service? {
let key = "\(type)\(name ?? "")"
if let entry = services[key] {
var resolvedService: Service?
if let instance = entry.instance {
resolvedService = instance as? Service
} else {
resolvedService = entry.factory(self) as? Service
entry.instance = resolvedService
}
return resolvedService
}
return nil
}
}

We created a Dependency Injection Container named DIContainer, which is located in the CoreModule.

public protocol NavigationService: ExternalNavigationService {
var navigationController: UINavigationController { get set }
var container: DIContainerService { get set }
func popToRootViewController(animated: Bool)
}



public protocol PresentableView {
func toPresent() -> UIViewController
}

public extension PresentableView where Self: UIViewController {
func toPresent() -> UIViewController {
return self
}
}


public protocol PresentableLoginView: PresentableView {}
public protocol PresentableBasketView: PresentableView {}
public protocol PresentablePaymentView: PresentableView {}



public protocol ExternalNavigationService {
func openLoginViewController()
func openBasketViewController()
func openPaymentViewController()
}

And then, we created a bunch of protocols in the NavigationService. These protocols will be used in the App Navigation.

Each module’s navigation service is defined by a protocol that specifies the methods for navigating the corresponding view controller. These protocols can be implemented by separate modules or by a single navigation service that handles the navigation between the modules.

import CoreModule


public protocol LoginNavigationService {
var container: DIContainerService { get set }
var navigationController: UINavigationController? { get set }
func openLoginViewController()
func openPhoneUpdateViewController()

func openBasketViewController()
}

public class LoginNavigation: LoginNavigationService {
public var container: CoreModule.DIContainerService
public var navigationController: UINavigationController?

public init(container: CoreModule.DIContainerService,
navigationController: UINavigationController? = nil) {
self.container = container
self.navigationController = navigationController
registerViewControllers()
}

private func registerViewControllers() {
container.register(type: PresentableLoginView.self) { container in
LoginViewController(navigationService: container.resolve(type: LoginNavigationService.self)!)
}
}

public func openLoginViewController() {
let viewController = container.resolve(type: PresentableLoginView.self)!
navigationController?.show(viewController.toPresent(), sender: nil)
}

public func openPhoneUpdateViewController() {

}

public func openBasketViewController() {
let appNavigationService = container.resolve(type: NavigationService.self)!
appNavigationService.openBasketViewController()
}

}

We created a class named LoginNavigation. This class handles navigation from the login module to other modules.

Navigation classes have two dependencies. The first is a container that resolves instances we will use in the next step, and the second is a navigation controller that manages the presenting and pushing of view controllers

We will use the same implementation for the BasketNavigation as well as the PaymentNavigation. See the codebase.

Next, let’s define the AppNavigation Service. The AppNavigation Service manages the navigation stack, passes dependencies between modules, and opens specific view controllers.

import CoreModule
import BasketModule
import LoginModule
import PaymentModule

class AppNavigation: NavigationService, ExternalNavigationService {

var navigationController: UINavigationController
var container: DIContainerService

init(navigationController: UINavigationController, container: DIContainerService) {
self.navigationController = navigationController
self.container = container
}

func openLoginViewController() {
let loginNavigationService = container.resolve(type: LoginNavigationService.self)!
let viewController = loginNavigationService.container.resolve(type: PresentableLoginView.self)!
navigationController.show(viewController.toPresent(), sender: nil)
}

func openBasketViewController() {
let basketNavigationService = container.resolve(type: BasketNavigationService.self)!
let viewController = basketNavigationService.container.resolve(type: PresentableBasketView.self)!
navigationController.show(viewController.toPresent(), sender: nil)
}

func openPaymentViewController() {
let paymentNavigationService = container.resolve(type: PaymentNavigationService.self)!
let viewController = paymentNavigationService.container.resolve(type: PresentablePaymentView.self)!
navigationController.show(viewController.toPresent(), sender: nil)
}

func popToRootViewController(animated: Bool) {
navigationController.popToRootViewController(animated: animated)
}

}

We created an AppNavigation class. As you see, App Navigation has an implementation similar to the above LoginNavigation example. The point here is that AppNavigation manages the navigation inter-module.

That’s all. You can see the full of code in my GitHub repository.

Conclusion

A Dependency Injection container can centralize the configuration and management of dependencies. This makes managing and modifying dependencies throughout the app easier while reducing coupling and increasing testability.

Overall, navigating between modules using Dependency Injection is a powerful way to maintain code maintainability and improve the flexibility of an iOS app. By separating the creation and use of objects, developers can create a more modular and extensible codebase that is easier to maintain and test.

--

--

Batikan Sosun
Batikan Sosun

Written by Batikan Sosun

Tweeting tips and tricks about #swift #xcode #apple Twitter @batikansosun Weekly Swift Blogging

Responses (2)