Programación en Swift y SwiftUI para iOS Developers

SwiftUI Toolbar – Tutorial con ejemplos

En el ecosistema del desarrollo moderno para iOS, la navegación y la jerarquía visual son los pilares de una buena experiencia de usuario (UX). Durante las primeras versiones de SwiftUI, los desarrolladores dependían de modificadores como .navigationBarItems para colocar botones en la barra de navegación. Sin embargo, Apple introdujo una evolución poderosa y semántica: el modifier .toolbar.

Este artículo es una inmersión profunda de 2000 palabras en el universo de las barras de herramientas en SwiftUI. No solo aprenderás a “poner un botón arriba a la derecha”; entenderás la filosofía detrás de la API, cómo gestionar la ubicación semántica de los elementos, cómo crear barras inferiores, personalizar teclados y adaptar tu interfaz a diferentes plataformas (iOS, iPadOS, macOS) con una sola base de código.


1. ¿Qué es exactamente el modifier .toolbar?

El modifier .toolbar es la forma unificada y declarativa de poblar las áreas de navegación y herramientas de una aplicación. A diferencia de sus predecesores, que estaban atados estrictamente a la “Barra de Navegación”, .toolbar es abstracto y adaptativo.

¿Por qué es importante esta distinción? Porque SwiftUI no piensa en píxeles fijos, sino en roles y contextos. Cuando usas .toolbar, no le estás diciendo al sistema “pon este botón en la coordenada (x,y)”. Le estás diciendo: “Tengo esta acción importante, encuéntrale el mejor lugar disponible según el dispositivo y el estado actual de la interfaz”.

La Estructura Básica

Para que una toolbar funcione, generalmente necesita residir dentro de un contexto de navegación, como un NavigationStack(o el obsoleto NavigationView) o un NavigationSplitView.

NavigationStack {
    Text("Mi Contenido Principal")
        .toolbar {
            // Aquí van tus botones
        }
}

2. Creando tu primera Toolbar: ToolbarItem

El bloque de construcción fundamental dentro del modifier .toolbar es el ToolbarItem. Un ToolbarItem envuelve la vista que quieres mostrar (un botón, un texto, una imagen) y, lo más importante, define su ubicación (placement).

Vamos a crear un ejemplo básico: una pantalla de perfil con un botón de “Ajustes”.

import SwiftUI

struct PerfilView: View {
    var body: some View {
        NavigationStack {
            VStack {
                Image(systemName: "person.circle.fill")
                    .font(.system(size: 100))
                    .foregroundStyle(.blue)
                Text("Usuario Invitado")
                    .font(.title)
            }
            .navigationTitle("Mi Perfil")
            .toolbar {
                ToolbarItem(placement: .primaryAction) {
                    Button {
                        print("Abrir ajustes")
                    } label: {
                        Image(systemName: "gear")
                    }
                }
            }
        }
    }
}

Desglosando el Código

  1. NavigationStack: Crea el entorno de la barra de navegación. Sin esto, la toolbar no tendría dónde renderizarse visualmente en la parte superior.
  2. .toolbar { ... }: El contenedor de nuestros items.
  3. placement: .primaryAction: Aquí está la magia. No dijimos “derecha” (trailing). Dijimos “acción primaria”. En iOS, esto se coloca automáticamente a la derecha. En macOS, podría comportarse diferente. Usar roles semánticos hace tu código más resistente al futuro.

3. La Ciencia de la Ubicación: ToolbarItemPlacement

El parámetro placement es lo que separa a los novatos de los expertos en SwiftUI. Conocer las opciones de ubicación te permite crear interfaces complejas que se sienten nativas. Analicemos las más importantes:

A. Ubicaciones Direccionales (.topBarLeading y .topBarTrailing)

Estas son las más directas si vienes de UIKit.

  • .topBarLeading: El lado izquierdo de la barra (en idiomas de izquierda a derecha). Ideal para botones de “Cancelar” o menús laterales.
  • .topBarTrailing: El lado derecho. Ideal para “Guardar”, “Editar” o acciones principales.

(Nota: En versiones anteriores de iOS se usaba .navigationBarLeading, que ahora está deprecado en favor de .topBar... para mayor claridad).

B. El Rol Principal (.principal)

Esta ubicación coloca el contenido en el centro de la barra de navegación. Por defecto, iOS pone aquí el título (.navigationTitle), pero puedes sobrescribirlo.

Caso de uso: Quieres mostrar un logotipo de marca o un Picker (selector de segmentos) en lugar de un título de texto simple.

ToolbarItem(placement: .principal) {
    VStack {
        Text("Mi App").font(.headline)
        Text("Estado: En línea").font(.caption).foregroundStyle(.green)
    }
}

C. La Barra Inferior (.bottomBar)

Si asignas este placement, SwiftUI creará automáticamente una barra de herramientas en la parte inferior de la pantalla (similar a la de Safari en iOS). Es ideal para acciones secundarias que deben estar al alcance del pulgar.

ToolbarItem(placement: .bottomBar) {
    HStack {
        Button("Anterior") { ... }
        Spacer()
        Button("Siguiente") { ... }
    }
}

D. El Teclado Virtual (.keyboard)

Este es uno de los trucos más útiles y menos conocidos. Puedes inyectar botones directamente en la barra de accesorios del teclado virtual.

Problema común: Tienes un formulario largo y quieres un botón de “Listo” para cerrar el teclado numérico. Solución:

TextField("Ingresa cantidad", text: $cantidad)
    .keyboardType(.decimalPad)
    .toolbar {
        ToolbarItem(placement: .keyboard) {
            HStack {
                Spacer() // Empuja el botón a la derecha
                Button("Listo") {
                    // Cerrar teclado
                    UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
                }
            }
        }
    }

4. Ubicaciones Semánticas: Hablando el idioma del Sistema

Apple recomienda encarecidamente usar ubicaciones semánticas en lugar de direccionales siempre que sea posible. Esto permite que el sistema reordene los botones si cambia la convención de diseño de iOS en el futuro.

  1. .cancellationAction: El sistema sabe que esto cancela la operación actual. Generalmente se ubica a la izquierda (leading).
  2. .confirmationAction: Confirma la operación (ej. “Guardar”, “Enviar”). Se ubica a la derecha y, a menudo, usa un peso de fuente en negrita automáticamente.
  3. .destructiveAction: Para acciones que borran datos. El sistema podría colorearlo en rojo o colocarlo separado de otras acciones seguras.
  4. .status: Muestra información de estado, útil en barras inferiores (ej. “Actualizado hace 5 min”).

Ejemplo de Modal de Edición:

struct EditarNotaView: View {
    @Environment(\.dismiss) var dismiss
    
    var body: some View {
        NavigationStack {
            TextEditor(text: .constant("Escribe aquí..."))
                .toolbar {
                    ToolbarItem(placement: .cancellationAction) {
                        Button("Cancelar") { dismiss() }
                    }
                    ToolbarItem(placement: .confirmationAction) {
                        Button("Guardar") { 
                            // Lógica de guardado
                            dismiss() 
                        }
                    }
                }
        }
    }
}

5. Agrupando Elementos: ToolbarItemGroup

¿Qué pasa si necesitas tres botones a la derecha? Si creas tres ToolbarItem separados, puede funcionar, pero el código se vuelve verboso. Para esto existe ToolbarItemGroup.

Este contenedor permite definir un único placement para múltiples vistas.

.toolbar {
    ToolbarItemGroup(placement: .topBarTrailing) {
        Button(action: {}) { Image(systemName: "heart") }
        Button(action: {}) { Image(systemName: "square.and.arrow.up") }
        Button(action: {}) { Image(systemName: "plus") }
    }
}

SwiftUI se encargará de espaciarlos correctamente según las guías de diseño de Apple (Human Interface Guidelines).


6. Personalización Visual Avanzada

Una vez que dominas la colocación, el siguiente paso es el estilo. Desde iOS 16, tenemos mucho más control sobre la apariencia de la barra.

Visibilidad de la Barra

A veces quieres una experiencia inmersiva donde la barra desaparece.

.toolbar(showToolbar ? .visible : .hidden, for: .navigationBar)

Fondos y Colores (toolbarBackground)

Antes, cambiar el color de fondo de la barra de navegación requieria “hacks” con UINavigationBar.appearance(). Ahora es nativo:

.toolbarBackground(.visible, for: .navigationBar)
.toolbarBackground(Color.indigo, for: .navigationBar)
.toolbarColorScheme(.dark, for: .navigationBar) // Fuerza texto blanco

Este código garantiza que la barra sea siempre visible (no transparente al hacer scroll) y tenga un color Índigo, forzando los botones y títulos a ser blancos (.dark scheme) para mantener el contraste.

Títulos de Menú

Si estás creando un menú desplegable dentro de la toolbar, puedes controlar cómo se ve el botón principal.

ToolbarItem(placement: .topBarTrailing) {
    Menu {
        Button("Duplicar") { }
        Button("Renombrar") { }
    } label: {
        // Personalización del botón que abre el menú
        Label("Opciones", systemImage: "ellipsis.circle")
    }
}

7. Gestión de Estado y Lógica

Una toolbar estática no sirve de mucho. Necesitamos que reaccione al estado de la aplicación. La belleza de SwiftUI es que el modifier .toolbar se redibuja cuando cambia el @State de la vista.

Ejemplo: Botón de “Guardar” deshabilitado hasta que haya texto.

struct FormularioView: View {
    @State private var nombre: String = ""
    @State private var guardando: Bool = false
    
    var body: some View {
        NavigationStack {
            Form {
                TextField("Tu nombre", text: $nombre)
            }
            .navigationTitle("Registro")
            .toolbar {
                ToolbarItem(placement: .confirmationAction) {
                    if guardando {
                        ProgressView()
                    } else {
                        Button("Enviar") {
                            guardarDatos()
                        }
                        .disabled(nombre.isEmpty) // Validación reactiva
                    }
                }
            }
        }
    }
    
    func guardarDatos() {
        guardando = true
        // Simulación de red
        DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
            guardando = false
        }
    }
}

En este ejemplo, vemos dos patrones clave:

  1. Validación: El modificador .disabled lee la variable nombre. Si está vacía, el botón de la toolbar se atenúa automáticamente.
  2. Cambio de Estado: Cuando guardando es true, reemplazamos el botón por un ProgressView (spinner). Esto ocurre suavemente dentro de la misma barra.

8. Consideraciones Multiplataforma (iOS vs iPadOS vs macOS)

Uno de los mayores beneficios de .toolbar es su adaptabilidad.

  • iOS (iPhone): Las acciones .primaryAction o .topBarTrailing suelen ir arriba a la derecha. Si hay muchas, iOS podría colapsarlas automáticamente o reducir el espacio del título.
  • iPadOS: Dependiendo del tamaño de la pantalla y si usas NavigationSplitView, los elementos de la toolbar pueden moverse dinámicamente.
  • macOS: Aquí la transformación es radical. Una toolbar en macOS reside fuera del contenido de la ventana principal (en la parte superior de la ventana del Finder/App). Los botones se renderizan como controles nativos de AppKit. El placement: .principal no funciona igual; en macOS, la toolbar es un espacio unificado.

Si estás desarrollando para macOS con SwiftUI, es vital agrupar comandos relacionados. El sistema intentará mostrar iconos sin texto a menos que especifiques lo contrario, y los menús contextuales son más comunes.


9. Errores Comunes y Mejores Prácticas

A lo largo de los años trabajando con SwiftUI, he visto ciertos errores recurrentes al usar toolbars. Aquí te explico cómo evitarlos:

A. Sobrecarga

Error: Poner 5 iconos en la barra superior. Solución: Usa como máximo 2 acciones primarias visibles. Para el resto, agrupalas en un Menu o muévelas a una .bottomBar. En pantallas móviles, el espacio horizontal es oro.

B. Áreas de Toque (Hit Targets)

Error: Usar solo texto pequeño para botones importantes. Solución: Asegúrate de que tus botones tengan suficiente área de toque. Aunque el sistema maneja esto bien por defecto, si usas vistas personalizadas complejas dentro de un ToolbarItem, podrías romper la accesibilidad. Usa Label("Texto", systemImage: "icono") siempre que sea posible, ya que se adapta mejor.

C. Jerarquía de Modificadores

Error: Aplicar .toolbar al NavigationStack en lugar de a la vista interna. Solución: El modifier debe ir pegado al contenido dentro del stack.

// INCORRECTO ❌
NavigationStack {
   Text("Hola")
}
.toolbar { ... } // Esto intenta añadir la toolbar al stack externo, a menudo falla o no se actualiza al navegar.

// CORRECTO ✅
NavigationStack {
   Text("Hola")
       .toolbar { ... } // La toolbar pertenece a esta vista específica.
}

D. Ignorar la Accesibilidad

Los botones de icono (ej. un icono de engranaje) no tienen texto visible. VoiceOver no sabrá qué decir a menos que lo especifiques. Solución:

Button { ... } label: {
    Image(systemName: "gear")
}
.accessibilityLabel("Abrir Configuración")

Sin embargo, si usas el tipo Label estándar o botones de texto, SwiftUI maneja esto automáticamente.


10. Ejemplo “Master Class”: Un Gestor de Tareas Completo

Para finalizar, vamos a combinar todo lo aprendido en un ejemplo realista. Crearemos una vista de lista de tareas que tiene:

  1. Título principal.
  2. Botón de filtrado (izquierda).
  3. Botón de añadir (derecha).
  4. Barra inferior con conteo de tareas.
  5. Barra de teclado para añadir tareas rápidamente.
import SwiftUI

struct TaskManagerView: View {
    @State private var tareas = ["Comprar leche", "Llamar al médico", "Pagar luz"]
    @State private var nuevaTarea = ""
    @State private var filtroActivo = false
    @FocusState private var isInputFocused: Bool
    
    var body: some View {
        NavigationStack {
            List {
                if filtroActivo {
                    Text("Mostrando solo tareas prioritarias (Simulado)")
                        .foregroundStyle(.secondary)
                }
                
                ForEach(tareas, id: \.self) { tarea in
                    Text(tarea)
                }
                .onDelete { tareas.remove(atOffsets: $0) }
                
                // Campo de entrada integrado
                TextField("Nueva tarea...", text: $nuevaTarea)
                    .focused($isInputFocused)
            }
            .navigationTitle("Mis Tareas")
            .toolbar {
                // 1. Botón de Filtro a la izquierda
                ToolbarItem(placement: .topBarLeading) {
                    Button {
                        filtroActivo.toggle()
                    } label: {
                        Image(systemName: filtroActivo ? "line.3.horizontal.decrease.circle.fill" : "line.3.horizontal.decrease.circle")
                    }
                }
                
                // 2. Botón de Edición estándar a la derecha
                ToolbarItem(placement: .topBarTrailing) {
                    EditButton() // SwiftUI provee este botón nativo
                }
                
                // 3. Barra inferior de estado
                ToolbarItem(placement: .bottomBar) {
                    HStack {
                        Text("\(tareas.count) Tareas")
                            .font(.caption)
                        Spacer()
                    }
                }
                
                // 4. Barra personalizada sobre el teclado
                ToolbarItemGroup(placement: .keyboard) {
                    Spacer()
                    Button("Añadir Tarea") {
                        if !nuevaTarea.isEmpty {
                            tareas.append(nuevaTarea)
                            nuevaTarea = ""
                            // Mantener el foco o cerrarlo según preferencia
                        }
                    }
                    .disabled(nuevaTarea.isEmpty)
                    .bold()
                }
            }
        }
    }
}

Análisis del Ejemplo

Este código demuestra la flexibilidad extrema de .toolbar. Tenemos elementos interactuando con el estado (filtroActivo), elementos nativos del sistema (EditButton), elementos informativos en la parte inferior (.bottomBar) y mejoras de productividad en el teclado (.keyboard). Todo declarado de forma limpia y mantenible.


Conclusión

El modifier .toolbar en SwiftUI no es solo una forma de poner botones; es un sistema de gestión de intenciones. Al adoptar este modificador y sus ToolbarItemPlacements semánticos, te aseguras de que tu aplicación:

  1. Se vea nativa en todas las versiones de iOS.
  2. Se adapte automáticamente a iPadOS y macOS.
  3. Sea accesible y fácil de navegar.

Dejar atrás el pensamiento de coordenadas fijas y abrazar la naturaleza declarativa de la toolbar es un paso esencial para madurar como desarrollador de SwiftUI.

Si tienes cualquier duda sobre este artículo, contacta conmigo y estaré encantado de ayudarte 🙂. Puedes contactar conmigo en mi perfil de X o en mi perfil de Instagram

Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Article

Domina las funciones en Swift - Tutorial con ejemplos

Next Article

Cómo activar y desactivar botones en SwiftUI

Related Posts