Navigation Modal Views Dynamically

Navigation Coordinator in SwiftUI: Managing Modal Views Dynamically

Handling modal views effectively is essential for creating smooth, intuitive user experiences in SwiftUI apps. A Navigation Coordinator allows us to have much more control over when and how modal views are presented, ensuring that our apps feel polished and professional.

Additionally, planning different routes for each modal view helps management capabilities when working with complex navigation patterns.

SwiftUI Logo
SwiftUI - Apple's declarative framework for modern interfaces

Why use a Modal Coordinator?

Modal presentation is frequently used for presenting screens, including alerts, onboarding, configuration sheets, and more. Here are the key reasons to use a Modal Coordinator:

• Modal Coordinator helps us: • Manage modal presentation logic from the UI • Ensure consistency across different modal types • Provide clean dismiss logic separation from UI • Handle modal navigation state correctly across different views

Step 1: Define a Protocol for Modal Views

Creating a protocol allows SwiftUI UI to be built from the controller and displayed as a modal while maintaining the same structure throughout the app.

This will also help ensure that our presented modal has exactly the same behavior in the app as in the presentation.

import SwiftUI

enum ModalType: Identifiable, Hashable {
    case settings
    case profile
    case addItem
    case editItem(id: String)
    case imageViewer(image: String)
    case alert(title: String, message: String)
    
    var id: String {
        switch self {
        case .settings:
            return "settings"
        case .profile:
            return "profile"
        case .addItem:
            return "addItem"
        case .editItem(let id):
            return "editItem-\(id)"
        case .imageViewer(let image):
            return "imageViewer-\(image)"
        case .alert(let title, _):
            return "alert-\(title)"
        }
    }
}

Step 2: Create the Modal Coordinator

The coordinator will maintain the state of modal views and provide methods to present and dismiss modals:

import SwiftUI

class ModalCoordinator: ObservableObject {
    @Published var activeModal: ModalType?
    @Published var isPresented: Bool = false
    
    func presentModal(_ modal: ModalType) {
        activeModal = modal
        isPresented = true
    }
    
    func dismissModal() {
        isPresented = false
        activeModal = nil
    }
    
    @ViewBuilder
    func modalView() -> some View {
        if let modal = activeModal {
            switch modal {
            case .settings:
                SettingsView()
            case .profile:
                ProfileView()
            case .addItem:
                AddItemView()
            case .editItem(let id):
                EditItemView(itemId: id)
            case .imageViewer(let image):
                ImageViewerView(imageName: image)
            case .alert(let title, let message):
                AlertView(title: title, message: message)
            }
        }
    }
}

Step 3: Implement the Main View Using the Coordinator

The main view will use the coordinator to handle modal presentation dynamically:

struct ContentView: View {
    @StateObject private var modalCoordinator = ModalCoordinator()
    
    var body: some View {
        NavigationView {
            VStack(spacing: 20) {
                Text("Main Content View")
                    .font(.largeTitle)
                    .padding()
                
                // Buttons to present different modals
                VStack(spacing: 15) {
                    Button("Show Settings") {
                        modalCoordinator.presentModal(.settings)
                    }
                    .buttonStyle(.borderedProminent)
                    
                    Button("Show Profile") {
                        modalCoordinator.presentModal(.profile)
                    }
                    .buttonStyle(.borderedProminent)
                    
                    Button("Add New Item") {
                        modalCoordinator.presentModal(.addItem)
                    }
                    .buttonStyle(.borderedProminent)
                    
                    Button("Edit Item") {
                        modalCoordinator.presentModal(.editItem(id: "123"))
                    }
                    .buttonStyle(.borderedProminent)
                    
                    Button("View Image") {
                        modalCoordinator.presentModal(.imageViewer(image: "sample-image"))
                    }
                    .buttonStyle(.borderedProminent)
                    
                    Button("Show Alert") {
                        modalCoordinator.presentModal(.alert(title: "Alert", message: "This is a modal alert"))
                    }
                    .buttonStyle(.borderedProminent)
                }
            }
            .navigationTitle("Modal Coordinator Demo")
            .sheet(isPresented: $modalCoordinator.isPresented) {
                modalCoordinator.modalView()
                    .environmentObject(modalCoordinator)
            }
        }
        .environmentObject(modalCoordinator)
    }
}

Step 4: Create the Modal Coordinator Views

Each modal view will have access to the coordinator so it can dismiss itself properly:

struct SettingsView: View {
    @EnvironmentObject var modalCoordinator: ModalCoordinator
    
    var body: some View {
        NavigationView {
            VStack {
                Text("Settings View")
                    .font(.title)
                
                Text("Configure your app settings here")
                    .padding()
                
                Spacer()
            }
            .navigationTitle("Settings")
            .navigationBarTitleDisplayMode(.inline)
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    Button("Done") {
                        modalCoordinator.dismissModal()
                    }
                }
            }
        }
    }
}

struct ProfileView: View {
    @EnvironmentObject var modalCoordinator: ModalCoordinator
    
    var body: some View {
        NavigationView {
            VStack {
                Text("Profile View")
                    .font(.title)
                
                Text("User profile information")
                    .padding()
                
                Spacer()
            }
            .navigationTitle("Profile")
            .navigationBarTitleDisplayMode(.inline)
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    Button("Close") {
                        modalCoordinator.dismissModal()
                    }
                }
            }
        }
    }
}

Step 5: Define Modal Views with Automatic Height

For smaller modal views, we can implement a system that automatically adjusts height:

struct AddItemView: View {
    @EnvironmentObject var modalCoordinator: ModalCoordinator
    @State private var itemName = ""
    @State private var itemDescription = ""
    
    var body: some View {
        VStack(spacing: 20) {
            Text("Add New Item")
                .font(.title2)
                .fontWeight(.bold)
            
            TextField("Item Name", text: $itemName)
                .textFieldStyle(RoundedBorderTextFieldStyle())
            
            TextField("Description", text: $itemDescription)
                .textFieldStyle(RoundedBorderTextFieldStyle())
            
            HStack(spacing: 15) {
                Button("Cancel") {
                    modalCoordinator.dismissModal()
                }
                .buttonStyle(.bordered)
                
                Button("Add Item") {
                    // Logic to add item
                    print("Adding item: \(itemName)")
                    modalCoordinator.dismissModal()
                }
                .buttonStyle(.borderedProminent)
            }
        }
        .padding()
        .presentationDetents([.medium])
    }
}

Step 6: Create the Modal Container View

A container view that handles more complex modal content with internal navigation:

struct ModalContainerView: View {
    @EnvironmentObject var modalCoordinator: ModalCoordinator
    
    var body: some View {
        Group {
            if let modal = modalCoordinator.activeModal {
                switch modal {
                case .settings, .profile:
                    // Modals that need full NavigationView
                    modalCoordinator.modalView()
                case .addItem, .editItem, .alert:
                    // Simpler modals without NavigationView
                    modalCoordinator.modalView()
                case .imageViewer:
                    // Full screen modal
                    modalCoordinator.modalView()
                        .presentationDetents([.large])
                }
            }
        }
    }
}

What Did We Achieve?

By using this Coordinator pattern, we've successfully created a robust, scalable modal navigation system for our SwiftUI application. Our solution provides:

💡
• Centralized Logic: All modal presentation logic is centralized in the coordinator • Flexibility: Ability to present different types of modals with various configurations • Maintainability: Easy to add new modal types or modify existing ones • Testability: Coordinator can be easily tested in isolation

Simply use the coordinator anywhere you need to present a modal:

// From any view with access to the coordinator
Button("Show Settings") {
    modalCoordinator.presentModal(.settings)
}

// Or with parameters
Button("Edit Item") {
    modalCoordinator.presentModal(.editItem(id: selectedItemId))
}

This pattern is particularly useful for complex applications where you need to present multiple types of modals with different behaviors and presentation styles. The coordinator provides a clean separation of concerns and makes your code more maintainable and testable.

ℹ️
Looking to implement comprehensive navigation in your SwiftUI app? Combine this modal coordinator with a navigation coordinator for a complete navigation solution.