¿Alguna vez has tenido que abrir if y modificar diez sentencias if solo para cambiar un estado de pantalla?
Cuando todos los estados de carga, éxito y error se gestionan con if isLoading y else if hasError, el código se vuelve inmanejable enseguida.
Hoy explicaré, basándome en experiencia real, cómo organizar el infierno de los if en objetos de estado con el patrón State de Swift.
La idea principal es esta: el patrón State consiste en separar cada estado en un objeto independiente (o enum) y colocar la lógica de transición dentro de ese objeto. Así, las ramas condicionales quedan centralizadas.
La diferencia con el patrón Strategy, cuya estructura es casi idéntica, está en quién impulsa el cambio. Patrón Strategy vs. patrón State permite distinguir primero la selección externa de la transición interna de estados.
Qué aprenderás
- Por qué aparece el infierno de los if
- Dos formas de dividir estados en objetos en Swift
- Cuándo usar enum o protocol
- Un orden de refactorización aplicable en la práctica
¿Por qué aparece el infierno de los if?
Al principio, con solo un par de estados, if parece suficiente. Pero al crecer los requisitos, aumentan a 5 o 6: carga, éxito, error, pantalla vacía, reintento y demás.
El problema es que estas combinaciones de estados se dispersan entre varios métodos. Se repiten en la lógica de activación de botones como if, en la actualización de pantalla como if y en los callbacks de red como if. Por eso, al añadir un estado hay que localizar y modificar cada bloque if; basta olvidar uno para crear un bug.
Cuando los estados están dispersos, los bugs se esconden; cuando se agrupan, salen a la luz.
El patrón State evita precisamente esta «dispersión».
Cómo dividir estados en objetos en Swift (con enum)
La forma más ligera de empezar es enum. Puedes definir los estados como valores y almacenar los datos necesarios mediante valores asociados. Este es un ejemplo de estados de pantalla representados con un enum.
enum ViewState {
case loading
case loaded(items: [String]) // Incluir datos en caso de éxito
case failed(message: String) // Incluir el motivo del error
case empty
}
Ahora la actualización de pantalla termina con switch una sola vez. Los valores asociados impiden que exista un estado ambiguo como «éxito con datos nil».
switch state {
case .loading: showSpinner()
case .loaded(let items): render(items)
case .failed(let msg): showError(msg)
case .empty: showEmptyView()
}
Para la mayoría de los estados de pantalla, el enfoque enum es suficiente. switch detecta los casos omitidos en compilación, así que añadir estados resulta más seguro.
enum vs. protocol: ¿cuándo usar cada uno?
Si el comportamiento de cada estado es muy distinto y las reglas de transición son complejas, conviene usar protocol. Cada estado se convierte en un tipo que devuelve por sí mismo el siguiente estado. Estas son las diferencias.
| Categoría | Enfoque enum | Enfoque protocol |
|---|---|---|
| Situación adecuada | Estados centrados en datos | Cada estado tiene un comportamiento distinto |
| Añadir un estado | Añadir un case | Añadir un tipo |
| Lógica de transición | switch externo | Encapsulada dentro de los objetos de estado |
| Curva de aprendizaje | Baja | Algo alta |
La clave del enfoque protocol es que cada objeto se responsabiliza de sus transiciones de estado.
protocol PlayerState {
func play() -> PlayerState // Devolver el siguiente estado
func pause() -> PlayerState
}
struct PlayingState: PlayerState {
func play() -> PlayerState { self } // Mantenerlo si ya se está reproduciendo
func pause() -> PlayerState { PausedState() } // Cambiar a pausa
}
Así, reglas como «¿qué ocurre si se pulsa pause mientras se reproduce?» existen únicamente dentro de ese estado. Desaparecen las ramas y cada estado solo conoce sus propias reglas.
Refactorización práctica: sigue este orden
Intentar cambiarlo todo de una vez puede intimidar. Yo hice la migración poco a poco y en este orden.
- Anotar la lista de estados que realmente representa el
ifdisperso - Definir esos estados en un único enum
- Sustituir primero el código de actualización de pantalla por
switch - Eliminar por completo con enum las combinaciones imposibles, como carga y error simultáneos
- Promover a protocol solo las partes con reglas de transición complejas
La clave es empezar con un enum y promoverlo a protocol solo cuando sea necesario. Diseñar todo con protocol desde el principio puede hacer que el código resulte más pesado.
Preguntas frecuentes (Q&A)
P. ¿Debo usar el patrón aunque solo haya 3 estados?
No es obligatorio. Sin embargo, si los estados están dispersos entre varios métodos, conviene agruparlos en un enum independientemente de cuántos haya.
P. ¿También se puede usar con SwiftUI?
Sí. Encaja muy bien si mantienes el estado como @Published var state: ViewState y haces la bifurcación en la vista mediante switch.
P. ¿No se vuelve desordenado un enum con muchos valores asociados?
Cuando superan los 3 valores asociados, recomiendo agruparlos en un struct independiente y almacenarlos como case loaded(Result).
Puede resultar extraño al principio, pero cuando aprendas a agrupar los estados en objetos no querrás volver al código anterior. Empieza organizando una pantalla pequeña con un enum. Saldrás del infierno de los if antes de lo que esperas. ¡Ánimo!

