Diseño de software

Implementar undo con el patrón Command de Swift

Al crear una app, la petición de «añadir solo un botón de undo» aparece con más frecuencia de la que parece.

4 min de lectura
Imagen de portada de Implementar undo con el patrón Command de Swift

Al crear una app, la petición de «añadir solo un botón de undo» aparece con más frecuencia de la que parece.

Al añadir undo, el código puede convertirse fácilmente en espagueti. Por un solo botón, acabas dispersando la lógica de reversión entre el código de borrado, movimiento y edición.

Quien pone orden aquí es el patrón Command de Swift.

En este artículo veremos por qué el patrón Command sirve de base para undo y repasaremos la estructura mínima de código para usarlo. El foco está en una forma que puedas integrar ahora mismo en tu proyecto, no en la teoría compleja.

¿Qué es el patrón Command? Resumen en una línea

Empecemos con una definición breve.

El patrón Command encapsula en un objeto la acción de «hacer algo» y agrupa la ejecución y la cancelación en una sola unidad.

En lugar de ejecutar la lógica al pulsar un botón, creamos un objeto Command que indique «haz esta acción».

Ese comando contiene tanto la forma de ejecutarse (execute) como la de revertirse (undo).

En resumen:

  1. Convertir las acciones en objetos: borrar, mover e introducir son comandos independientes
  2. Cada comando empareja execute() y undo()
  3. Apilar los comandos ejecutados
  4. Para deshacer, sacar un comando de la pila y llamar a undo()

Gracias a esta estructura, la lógica de reversión permanece dentro de cada comando en lugar de quedar dispersa.


Creemos primero el protocolo Command en Swift

Primero definimos el contrato, es decir, el protocolo que debe cumplir todo comando.

Este es el esqueleto mínimo que obliga a implementar la ejecución y el undo.

protocol Command {
    func execute()   // Ejecutar acción
    func undo()      // Deshacer ejecución
}

Solo necesitas estos dos métodos.

Ahora creemos un comando concreto que contenga una acción real, por ejemplo, uno que añada texto.

El código siguiente añade texto y lo elimina de nuevo al deshacer.

final class AddTextCommand: Command {
    private let document: Document
    private let text: String

    init(document: Document, text: String) {
        self.document = document
        self.text = text
    }

    func execute() { document.content += text }
    func undo()    { document.content.removeLast(text.count) }
}

execute añade el texto y undo elimina exactamente lo añadido. Que ejecución y undo estén juntos en un objeto es todo el patrón.

El protocolo Command empieza con solo dos líneas
El protocolo Command empieza con solo dos líneas

La clave de undo es la pila

Después de crear comandos, necesitas un «administrador» que los gestione. Este papel suele llamarse Invoker.

El administrador va apilando los comandos ejecutados.

Al pulsar undo, sacas el comando más reciente y llamas a undo(). Deshacer primero lo último que se hizo es el orden natural de undo.

Este es el esqueleto del administrador.

final class CommandManager {
    private var undoStack: [Command] = []

    func run(_ command: Command) {
        command.execute()
        undoStack.append(command)   // Guardar en la pila tras ejecutar
    }

    func undo() {
        guard let last = undoStack.popLast() else { return }
        last.undo()                 // Deshacer desde el último elemento
    }
}

El uso se vuelve muy sencillo.

En buttonTapped llama a manager.run(command), y desde el botón de undo solo llama a manager.undo().

CommandManager gestiona el orden de reversión y la pila, así que el código de la interfaz se mantiene limpio.

Las tres piezas de Command se conectan así
Las tres piezas de Command se conectan así

Si también necesitas redo, guarda los comandos deshechos aparte en redoStack. Como la estructura es simétrica, ampliarla resulta sencillo.


¿Cuándo usar y cuándo evitar el patrón Command?

El patrón Command tampoco es universal. Esta tabla resume cuándo encaja y cuándo resulta excesivo.

Situación Patrón Command
Se necesita undo y redo Muy adecuado
Hay que conservar el historial de operaciones Adecuado
Poner acciones en cola para ejecutarlas después Adecuado
Un botón sencillo sin nada que deshacer Excesivo; basta una función

El patrón Command brilla cuando necesitas deshacer o conservar un historial. Son ejemplos típicos las apps de dibujo, los editores de texto, la cancelación de pagos y el undo en juegos.

En cambio, aplicarlo a un botón que solo cambia de pantalla no hace más que aumentar el código.

Convertir todos los botones en comandos puede llenar rápidamente el proyecto de archivos, así que aplícalo solo a acciones que deban revertirse.

Una estructura que extrae y deshace las tarjetas una a una desde la de arriba
Una estructura que extrae y deshace las tarjetas una a una desde la de arriba

Resumen

Añadir undo puede parecer abrumador, pero con el patrón Command como base funciona de forma más limpia de lo esperado.

Empieza aplicando a una pantalla pequeña las tres piezas de hoy: protocolo, comandos concretos y administrador. Cuando la estructura te resulte familiar, ampliar con redo o historial será natural. Añade con confianza un botón de undo a tu app.

Seguir leyendo