DEV Community

Madhuri Latha Gondi
Madhuri Latha Gondi

Posted on

Implementing Descriptor-Driven Navigation Architecture (DDNA) in Modular iOS Applications

By Madhuri Latha Gondi | Mobile Architecture & iOS Platform Engineering


In modern iOS applications, navigation is no longer a simple transition between screens. Users can enter an app through multiple entry points such as push notifications, universal links, marketing campaigns, and in-app actions.

As applications grow larger and more modular, navigation logic often becomes fragmented and difficult to maintain.

In many codebases, navigation begins with something simple like:

navigationController?.pushViewController(DetailsViewController(), animated: true)

Enter fullscreen mode Exit fullscreen mode

While this works for small applications, large mobile systems quickly run into problems:

  1. tight coupling between feature modules
  2. inconsistent deep link handling
  3. navigation logic spread across many controllers
  4. poor testability

To address these challenges, I use an architectural approach called Descriptor-Driven Navigation Architecture (DDNA).
DDNA separates navigation intent from navigation implementation, allowing applications to scale navigation logic cleanly across modules.

What is Descriptor-Driven Navigation Architecture?
Instead of navigating directly to a screen, the application creates a navigation descriptor that represents the destination.
Example:

let descriptor = DeepLinkDescriptor(
 path: "/profile/details",
 parameters: ["userId": "12345"],
 presentationMode: .push
)
Enter fullscreen mode Exit fullscreen mode

This descriptor is then processed through a routing pipeline.

Entry Point
(UI / Push Notification / Universal Link)

DeepLinkDescriptor

NavigationManager

DeepLinkMapper

Feature ViewController

`This architecture makes navigation consistent across the application.

Step 1 — Define a Navigation Descriptor
The foundation of DDNA is the DeepLinkDescriptor object.

`
struct DeepLinkDescriptor {

enum PresentationMode {
case push
case modal
}

let path: String
let parameters: [String: String?]
let presentationMode: PresentationMode
}
`
This object represents navigation intent rather than a concrete screen.
Because it is independent of UIKit, descriptors can be created from:

  1. UI actions
  2. push notifications
  3. universal links
  4. background events

Step 2 — Implement a Navigation Manager
The NavigationManager coordinates routing.


protocol NavigationManaging {
func navigate(using descriptor: DeepLinkDescriptor)
}

Implementation:

`final class NavigationManager: NavigationManaging {

private let mapper: DeepLinkMapper
private weak var navigationContainer: NavigationContainer?

init(
mapper: DeepLinkMapper,
navigationContainer: NavigationContainer
) {
self.mapper = mapper
self.navigationContainer = navigationContainer
}

func navigate(using descriptor: DeepLinkDescriptor) {

guard let viewController = mapper.map(descriptor) else {
return
}

switch descriptor.presentationMode {

case .push:
navigationContainer?.push(viewController)

case .modal:
navigationContainer?.present(viewController)
}
}
}`

The NavigationManager orchestrates navigation without knowing the details of each feature module.

Step 3 — Map Descriptors to Screens
The DeepLinkMapper converts descriptors into actual destinations.


protocol DeepLinkMapper {
func map(_ descriptor: DeepLinkDescriptor) -> UIViewController?
}

Example implementation:

`
final class FeatureDeepLinkMapper: DeepLinkMapper {

func map(_ descriptor: DeepLinkDescriptor) -> UIViewController? {

switch descriptor.path {

case "/profile/details":

let userId = descriptor.parameters["userId"] ?? nil

return ProfileDetailsViewController(userId: userId)

default:
return nil
}
}
}
`
This isolates screen creation from navigation logic.

Step 4 — Introduce a Navigation Container
Instead of depending directly on UINavigationController, DDNA introduces a container abstraction.

`
protocol NavigationContainer: AnyObject {

func push(_ viewController: UIViewController)

func present(_ viewController: UIViewController)
}
`
Example implementation:

`
final class DefaultNavigationContainer: NavigationContainer {

weak var navigationController: UINavigationController?

func push(_ viewController: UIViewController) {
navigationController?.pushViewController(viewController, animated: true)
}

func present(_ viewController: UIViewController) {
navigationController?.present(viewController, animated: true)
}
}
`
This abstraction allows navigation to work with UIKit, SwiftUI bridges, or custom containers.

Step 5 — Assemble Using Dependency Injection
Instead of global singletons, dependencies are assembled at the application level.

`
final class NavigationManagerFactory {

static func make(
navigationController: UINavigationController
) -> NavigationManager {

let container = DefaultNavigationContainer()
container.navigationController = navigationController

let mapper = FeatureDeepLinkMapper()

return NavigationManager(
mapper: mapper,
navigationContainer: container
)
}
}
`
Dependency injection improves modularity and testability.

Example Usage
A feature module can trigger navigation using a descriptor.

`
let descriptor = DeepLinkDescriptor(
path: "/profile/details",
parameters: ["userId": "12345"],
presentationMode: .push
)

navigationManager.navigate(using: descriptor)
`
The feature does not need to know how the destination screen is constructed.

Supporting Universal Links
DDNA also simplifies deep link routing.

`
func descriptor(from url: URL) -> DeepLinkDescriptor {

let components = URLComponents(url: url, resolvingAgainstBaseURL: false)

var parameters: [String: String?] = [:]

components?.queryItems?.forEach {
parameters[$0.name] = $0.value
}

return DeepLinkDescriptor(
path: components?.path ?? "",
parameters: parameters,
presentationMode: .push
)
}
`
Now UI actions, push notifications, and universal links share the same navigation flow.

Benefits of DDNA
Descriptor-Driven Navigation Architecture provides several advantages:

  1. modular feature isolation
  2. centralized navigation logic
  3. consistent deep linking
  4. improved testability
  5. scalable mobile architecture This approach is particularly useful for enterprise mobile applications with multiple feature teams.

Final Thoughts
Navigation often becomes one of the most complex parts of a mobile architecture as applications grow.
By separating navigation intent from navigation execution, DDNA allows teams to build scalable routing systems while maintaining modular codebases.
Treating navigation as a platform capability rather than a controller detail enables mobile systems to scale more effectively as products evolve.

Top comments (0)