Coordinadores de Navegación: TabBar Independientes

Coordinadores de Navegación en SwiftUI: Implementando un TabBar con Stacks Independientes

SwiftUI Logo
SwiftUI - Framework declarativo de Apple

La navegación en SwiftUI puede volverse compleja a medida que tu aplicación crece. Usar un Coordinador de Navegación nos permite gestionar mejor las rutas y mantener el código organizado. En este artículo, exploraremos cómo implementar un coordinador de navegación robusto para una aplicación de iOS que navega con una barra de pestañas, y aprende a mantener las vistas con stacks independientes que no pierdan su navegación previamente realizada.

¿Por qué usar un Coordinador de Navegación?

Existen varias razones por las cuales es buena práctica usar un Coordinador de Navegación. Los principales beneficios son:

• Mantener la navegación modular y organizada. • Preservar el estado de navegación de cada pantalla. • Separar la lógica de navegación del UI.

Paso 1: Definir las Rutas

Creamos un enum para representar las diferentes pantallas de la app:

enum Routes: Hashable {
    case Home
    case Detail
    case Settings
    case Profile
    case Notifications
}

Paso 2: Crear el Coordinador de Navegación

El coordinador mantendrá las rutas y creará las navegaciones:

import SwiftUI

class NavigationCoordinator: ObservableObject {
    @Published var path = NavigationPath()
    
    func push(_ route: Routes) {
        path.append(route)
    }
    
    func goToRoot() {
        path.removeLast(path.count)
    }
    
    func pop() {
        if !path.isEmpty {
            path.removeLast()
        }
    }
    
    @ViewBuilder
    func build(route: Routes) -> some View {
        switch route {
        case .Home:
            HomeView()
        case .Detail:
            DetailView()
        case .Settings:
            SettingsView()
        case .Profile:
            ProfileView()
        case .Notifications:
            NotificationsView()
        }
    }
}

Paso 3: Crear la Vista de TabBar con Coordinadores

Cada pantalla tendrá su propio coordinador independiente:

struct MainTabBarView: View {
    @StateObject private var homeCoordinator = NavigationCoordinator()
    @StateObject private var settingsCoordinator = NavigationCoordinator()
    @StateObject private var profileCoordinator = NavigationCoordinator()
    @StateObject private var notificationsCoordinator = NavigationCoordinator()
    
    var body: some View {
        TabView {
            NavigationStack(path: $homeCoordinator.path) {
                homeCoordinator.build(route: .Home)
                    .navigationDestination(for: Routes.self) { route in
                        homeCoordinator.build(route: route)
                    }
            }
            .environmentObject(homeCoordinator)
            .tabItem {
                Label("Home", systemImage: "house")
            }
            
            NavigationStack(path: $settingsCoordinator.path) {
                settingsCoordinator.build(route: .Settings)
                    .navigationDestination(for: Routes.self) { route in
                        settingsCoordinator.build(route: route)
                    }
            }
            .environmentObject(settingsCoordinator)
            .tabItem {
                Label("Settings", systemImage: "gearshape")
            }
            
            NavigationStack(path: $profileCoordinator.path) {
                profileCoordinator.build(route: .Profile)
                    .navigationDestination(for: Routes.self) { route in
                        profileCoordinator.build(route: route)
                    }
            }
            .environmentObject(profileCoordinator)
            .tabItem {
                Label("Profile", systemImage: "person")
            }
            
            NavigationStack(path: $notificationsCoordinator.path) {
                notificationsCoordinator.build(route: .Notifications)
                    .navigationDestination(for: Routes.self) { route in
                        notificationsCoordinator.build(route: route)
                    }
            }
            .environmentObject(notificationsCoordinator)
            .tabItem {
                Label("Notifications", systemImage: "bell")
            }
        }
    }
}
💡
Cada pestaña mantiene su navegación independiente. Con este patrón puedes navegar con una vista independiente:
TabView {
    NavigationStack(path: $homeCoordinator.path) {
        homeCoordinator.build(route: .Home)
            .navigationDestination(for: Routes.self) { route in
                homeCoordinator.build(route: route)
            }
    }
    .environmentObject(homeCoordinator)
    .tabItem {
        Label("Home", systemImage: "house")
    }
    
    NavigationStack(path: $settingsCoordinator.path) {
        settingsCoordinator.build(route: .Settings)
            .navigationDestination(for: Routes.self) { route in
                settingsCoordinator.build(route: route)
            }
    }
    .environmentObject(settingsCoordinator)
    .tabItem {
        Label("Settings", systemImage: "gearshape")
    }
    
    NavigationStack(path: $profileCoordinator.path) {
        profileCoordinator.build(route: .Profile)
            .navigationDestination(for: Routes.self) { route in
                profileCoordinator.build(route: route)
            }
    }
    .environmentObject(profileCoordinator)
    .tabItem {
        Label("Profile", systemImage: "person")
    }
    
    NavigationStack(path: $notificationsCoordinator.path) {
        notificationsCoordinator.build(route: .Notifications)
            .navigationDestination(for: Routes.self) { route in
                notificationsCoordinator.build(route: route)
            }
    }
    .environmentObject(notificationsCoordinator)
    .tabItem {
        Label("Notifications", systemImage: "bell")
    }
}

Paso 4: Definir las Vistas

Estas son las vistas que se accederán en cada pantalla:

struct HomeView: View {
    @EnvironmentObject var coordinator: NavigationCoordinator
    
    var body: some View {
        VStack {
            Text("Home")
            Button("Go to Detail") {
                coordinator.push(.Detail)
            }
        }
    }
}

struct DetailView: View {
    @EnvironmentObject var coordinator: NavigationCoordinator
    
    var body: some View {
        VStack {
            Text("Detail")
            Button("Go to Settings") {
                coordinator.push(.Settings)
            }
        }
    }
}

struct SettingsView: View {
    @EnvironmentObject var coordinator: NavigationCoordinator
    
    var body: some View {
        VStack {
            Text("Settings")
            Button("Go to Profile") {
                coordinator.push(.Profile)
            }
        }
    }
}

struct ProfileView: View {
    @EnvironmentObject var coordinator: NavigationCoordinator
    
    var body: some View {
        VStack {
            Text("Profile")
            Button("Go to Notifications") {
                coordinator.push(.Notifications)
            }
        }
    }
}

struct NotificationsView: View {
    @EnvironmentObject var coordinator: NavigationCoordinator
    
    var body: some View {
        VStack {
            Text("Notifications")
            Button("Go to Home") {
                coordinator.push(.Home)
            }
        }
    }
}

Paso 5: Usar el Coordinador en la App

Finalmente, usamos el coordinador principal en la aplicación principal:

@main
struct NavigationApp: App {
    var body: some Scene {
        WindowGroup {
            MainTabBarView()
        }
    }
}

Conclusión

Listo, de esta manera podemos ver que la navegación para una aplicación, que te permitirá fácil una cadena, que te permitirá organizar mejor el proyecto complejo.

ℹ️
Con este patrón Coordinator podrás mantener la navegación de cada tab completamente independiente, mejorando la experiencia del usuario y organizando mejor tu código SwiftUI.
💡
¿Te gustó este tutorial? Puedes encontrar más contenido sobre desarrollo iOS y SwiftUI en mi perfil.