Programación en Swift y SwiftUI para iOS Developers

Cómo activar y desactivar botones en SwiftUI

En el desarrollo de aplicaciones modernas para iOS, la interfaz de usuario (UI) no es solo una colección de elementos estáticos; es una conversación dinámica con el usuario. Uno de los elementos más críticos de esta conversación es el botón.

Un botón no debería ser simplemente “clicable”. Debe comunicar contexto. ¿Puede el usuario continuar? ¿Faltan datos en el formulario? ¿Se está procesando una solicitud? En el antiguo UIKit (desarrollo imperativo), activar y desactivar botones requería manipular referencias directas y lógica dispersa. En SwiftUI, gracias a su naturaleza declarativa, este proceso es mucho más intuitivo, aunque requiere un cambio de mentalidad.

En este tutorial exhaustivo, exploraremos no solo cómo usar el modificador .disabled() para activar y desactivar botones en SwiftUI, sino cómo orquestar la lógica de validación, mejorar la experiencia de usuario (UX) con feedback visual y arquitecturar tu código para que sea mantenible y escalable.


1. El Cambio de Paradigma: Declarativo vs. Imperativo

Antes de escribir código, es vital entender la filosofía detrás de SwiftUI.

En el pasado (UIKit), tú eras el responsable de cambiar el botón.

“Si el campo de texto cambia, verifica la longitud. Si es mayor a 5, busca el botón ‘Enviar’ y pon su propiedad isEnabled en true“.

En SwiftUI, tú describes el estado del botón.

“El botón está desactivado si la longitud del texto es menor a 5”.

La diferencia es sutil pero poderosa: La interfaz es una función del estado.


2. La Implementación Básica

La forma más sencilla de controlar la interactividad de un botón es mediante el modificador .disabled(_:). Este modificador toma un valor booleano: si es true, el botón deja de responder a los toques y, por defecto, SwiftUI reduce su opacidad para indicar visualmente su estado.

Ejemplo: El Interruptor Simple

Imaginemos un escenario donde tenemos un “Toggle” (interruptor) maestro que controla si se puede pulsar un botón de acción.

import SwiftUI

struct BasicButtonExample: View {
    // 1. Estado que controla la lógica
    @State private var isLocked: Bool = true

    var body: some View {
        VStack(spacing: 20) {
            // Control para cambiar el estado
            Toggle("Bloquear botón", isOn: $isLocked)
                .padding()

            // 2. El Botón
            Button(action: {
                print("¡Acción ejecutada!")
            }) {
                Text("Ejecutar Acción")
                    .font(.headline)
                    .frame(maxWidth: .infinity)
                    .padding()
                    .background(Color.blue)
                    .foregroundColor(.white)
                    .cornerRadius(10)
            }
            // 3. El Modificador Mágico
            .disabled(isLocked)
            // Opcional: Cambiar la opacidad manualmente si se desea más control
            .opacity(isLocked ? 0.5 : 1.0) 
            .padding()
        }
    }
}

Análisis del código:

  • @State: Es la fuente de la verdad. Cuando isLocked cambia, SwiftUI redibuja la vista automáticamente.
  • .disabled(isLocked): Si isLocked es verdadero, el botón se desactiva. El cierre action no se ejecutará nunca, protegiendo tu lógica de negocio.

3. Casos de Uso Real: Validación de Formularios

Raramente desactivamos un botón con un interruptor manual. Lo más común es que el estado del botón dependa de la integridad de los datos ingresados por el usuario.

Vamos a crear una pantalla de “Login” simple. El botón de “Entrar” solo debe estar activo si:

  1. El campo de usuario no está vacío.
  2. La contraseña tiene al menos 6 caracteres.

Lógica en la Vista (Enfoque Inicial)

Para aplicaciones muy pequeñas, podemos usar propiedades computadas (computed properties) dentro de la misma Vista.

struct LoginView: View {
    @State private var username = ""
    @State private var password = ""

    // Propiedad computada para la validación
    var isFormValid: Bool {
        return !username.isEmpty && password.count >= 6
    }

    var body: some View {
        VStack(spacing: 20) {
            TextField("Usuario", text: $username)
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .autocapitalization(.none)

            SecureField("Contraseña", text: $password)
                .textFieldStyle(RoundedBorderTextFieldStyle())

            Button("Iniciar Sesión") {
                performLogin()
            }
            .buttonStyle(.borderedProminent)
            // Desactivamos si el formulario NO es válido
            .disabled(!isFormValid)
        }
        .padding()
    }

    func performLogin() {
        print("Logueando a \(username)...")
    }
}

¿Por qué esto es mejor que UIKit?

En UIKit, tendrías que suscribirte a los eventos de cambio de texto de ambos campos y llamar a una función de validación cada vez. Aquí, simplemente definimos isFormValid y SwiftUI se encarga de recalcularlo cada vez que username o passwordcambian.


4. Diseño y UX: Comunicación Visual

Un botón desactivado que se ve exactamente igual que uno activo es un error de diseño grave (Dark Pattern). El usuario pulsará el botón frustrado pensando que la app no funciona.

Aunque .disabled() aplica una opacidad por defecto, a menudo queremos un control más granular sobre el diseño.

Personalización con ButtonStyle

La forma más limpia de manejar estilos en SwiftUI es creando un ButtonStyle personalizado. Esto nos permite encapsular la lógica visual basándonos en si el botón está presionado o desactivado.

struct PrimaryButtonStyle: ButtonStyle {
    // Variable para saber si está desactivado desde el entorno
    @Environment(\.isEnabled) private var isEnabled

    func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .padding()
            .frame(maxWidth: .infinity)
            // Color condicional
            .background(isEnabled ? Color.blue : Color.gray.opacity(0.3))
            .foregroundColor(isEnabled ? .white : .gray)
            .cornerRadius(8)
            // Efecto de escala al pulsar (solo si está habilitado)
            .scaleEffect(configuration.isPressed ? 0.98 : 1.0)
            .animation(.easeInOut(duration: 0.2), value: isEnabled)
    }
}

// Uso:
Button("Enviar") { ... }
    .buttonStyle(PrimaryButtonStyle())
    .disabled(text.isEmpty)

Nota Clave: Usar @Environment(\.isEnabled) dentro del estilo es el secreto para que el estilo reaccione automáticamente al modificador .disabled() aplicado en la vista padre.


5. Arquitectura Profesional: MVVM (Model-View-ViewModel)

Cuando la lógica de validación se vuelve compleja (validar emails con RegEx, verificar coincidencia de contraseñas, aceptar términos y condiciones), el código dentro de la View se vuelve sucio (“Spaghetti Code”).

Aquí es donde entra MVVM. Movemos la lógica de estado y validación a una clase separada.

Paso 1: El ViewModel

Creamos una clase que conforme a ObservableObject. Esta clase será la “dueña” de los datos del formulario.

import Combine

class RegistrationViewModel: ObservableObject {
    // Inputs del usuario
    @Published var email = ""
    @Published var password = ""
    @Published var confirmPassword = ""
    @Published var agreedToTerms = false

    // Lógica de validación
    var isValid: Bool {
        // 1. Email básico
        guard email.contains("@") && email.contains(".") else { return false }
        
        // 2. Contraseña segura
        guard password.count >= 8 else { return false }
        
        // 3. Coincidencia
        guard password == confirmPassword else { return false }
        
        // 4. Términos
        guard agreedToTerms else { return false }
        
        return true
    }
}

Paso 2: La Vista Limpia

Ahora nuestra vista es extremadamente sencilla y solo se preocupa de mostrar datos.

struct RegistrationView: View {
    // Inyectamos el ViewModel
    @StateObject private var viewModel = RegistrationViewModel()

    var body: some View {
        Form {
            Section(header: Text("Cuenta")) {
                TextField("Email", text: $viewModel.email)
                SecureField("Contraseña", text: $viewModel.password)
                SecureField("Confirmar Contraseña", text: $viewModel.confirmPassword)
            }

            Section {
                Toggle("Acepto los términos", isOn: $viewModel.agreedToTerms)
            }

            Section {
                Button(action: {
                    // Lógica de envío
                    print("Registrando...")
                }) {
                    Text("Crear Cuenta")
                }
                // La vista solo lee 'viewModel.isValid'
                .disabled(!viewModel.isValid)
            }
        }
    }
}

Este enfoque hace que tu código sea testearle. Puedes escribir Pruebas Unitarias (Unit Tests) para RegistrationViewModel sin necesidad de cargar la interfaz gráfica.


6. Llevándolo al Siguiente Nivel: Feedback Táctil y Errores

A veces, simplemente desactivar el botón no es suficiente. El usuario puede preguntarse: “¿Por qué no puedo pulsar esto?”.

Una tendencia moderna en UX es dejar el botón habilitado, pero si los datos son inválidos, mostrar un error o una animación de “sacudida” (shake) al pulsarlo.

Ejemplo: Botón con Validación al Pulsar

struct InteractiveValidationButton: View {
    @State private var text = ""
    @State private var attempts: Int = 0 // Para animar
    @State private var showError = false

    var body: some View {
        VStack {
            TextField("Ingresa 'SwiftUI'", text: $text)
                .textFieldStyle(.roundedBorder)
                .padding()
                .border(showError ? Color.red : Color.clear)

            if showError {
                Text("El texto debe ser 'SwiftUI'")
                    .foregroundColor(.red)
                    .font(.caption)
            }

            Button("Verificar") {
                if text == "SwiftUI" {
                    print("Éxito")
                    showError = false
                } else {
                    // Feedback de error
                    withAnimation(.default) {
                        attempts += 1
                        showError = true
                    }
                }
            }
            .padding()
            .background(Color.blue)
            .foregroundColor(.white)
            .cornerRadius(8)
            // Animación de sacudida
            .modifier(ShakeEffect(animatableData: CGFloat(attempts)))
        }
    }
}

// Modificador geométrico para el efecto Shake (simplificado)
struct ShakeEffect: GeometryEffect {
    var amount: CGFloat = 10
    var shakesPerUnit = 3
    var animatableData: CGFloat

    func effectValue(size: CGSize) -> ProjectionTransform {
        ProjectionTransform(CGAffineTransform(translationX:
            amount * sin(animatableData * .pi * CGFloat(shakesPerUnit)),
            y: 0))
    }
}

Nota: Este enfoque es diferente a desactivar el botón. En lugar de prevenir la acción, permites la acción pero manejas el error. Elige el enfoque según el contexto de tu app.


7. Accesibilidad (A11y): El factor olvidado

Cuando desactivas un botón visualmente, también debes asegurarte de que los usuarios que utilizan tecnologías de asistencia (como VoiceOver) entiendan lo que sucede.

Afortunadamente, el modificador .disabled() de SwiftUI maneja esto bastante bien por defecto. VoiceOver anunciará el botón como “Atenuado” o “No disponible”.

Sin embargo, para una experiencia de primera clase, considera explicar por qué está desactivado si el usuario intenta interactuar con él, o usa .accessibilityHint() en los campos de texto para indicar los requisitos.

Button("Entrar") { ... }
    .disabled(!isValid)
    .accessibilityLabel("Botón de entrar")
    .accessibilityValue(!isValid ? "Desactivado, verifica los campos" : "Habilitado")

8. Errores Comunes y Soluciones

Error A: El orden de los modificadores

En SwiftUI, el orden importa. Si aplicas un .gesture (como un tap) después del .disabled(), a veces el gesto podría capturarse.Regla de oro: Coloca .disabled() al final de la cadena de modificadores del botón, pero antes de cualquier modificador de posición (padding, etc.) si quieres que el padding también sea “no interactivo” (aunque esto es menos relevante en botones estándar).

Error B: Lógica compleja en el body

Evita poner sentencias if complejas directamente dentro del modificador .disabled().

  • Mal: .disabled(email.count > 5 && password.count > 3 || !terms)
  • Bien: .disabled(!isFormValid) (usando una variable computada). Esto mejora la legibilidad enormemente.

Error C: Olvidar el feedback de color

Si usas un estilo de botón totalmente personalizado (usando .background(Color.red) directo en el label), el botón no se pondrá gris automáticamente al desactivarse. Debes manejar el color manualmente como vimos en la sección de ButtonStyle.


Conclusión

Activar y desactivar botones en SwiftUI y Xcode es una puerta de entrada para entender la gestión de estados reactivos. Hemos pasado de simplemente escribir .disabled(true) a construir una arquitectura MVVM robusta que separa la lógica de la vista y mejora la experiencia del usuario.

Resumen de puntos clave:

  1. Usa .disabled(_:) para control básico.
  2. Usa @State y propiedades computadas para lógica simple.
  3. Adopta MVVM y ObservableObject para formularios complejos.
  4. Implementa ButtonStyle para adaptar el diseño visual al estado (activo/inactivo).
  5. Nunca olvides la Accesibilidad.

El control de estado es lo que diferencia una aplicación que “funciona” de una que “se siente bien”. Al dominar estos conceptos, estás un paso más cerca de ser un desarrollador iOS experto.

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

SwiftUI Toolbar - Tutorial con ejemplos

Next Article

Cómo usar la Dynamic Island del iPhone en SwiftUI

Related Posts