Compilación condicional en Swift
Qué es #if en Swift, cómo difiere de un if normal, y cuándo usarlo: DEBUG, plataformas, simulador, flags propios y la diferencia con assert().
En el post de feature flags usamos #if DEBUG para envolver el log de flags activos. Lo pusimos, funcionó, y seguimos. Pero nunca expliqué qué está pasando ahí exactamente — y vale la pena, porque #if en Swift es diferente a cualquier otra condición que uses en el día a día.
No es un if. No evalúa nada en runtime. Lo que hace es decirle al compilador qué código incluir y qué ignorar según la configuración de build. Eso cambia bastante las cosas.
Compile-time vs runtime
Un if normal evalúa una condición cuando el código se ejecuta. Ambas ramas existen en el binario — el compilador las incluye aunque una nunca se tome.
Un bloque #if funciona antes de eso. El compilador lee la condición, y el código que no la cumple no llega al binario. No se optimiza, no se ignora en runtime — directamente no existe.
// Ambas ramas están en el binario, aunque isDebug siempre sea false en release
if isDebug {
logActiveFlags()
}
// En release, logActiveFlags() no existe en absoluto
#if DEBUG
logActiveFlags()
#endif
Eso importa cuando hay código que definitivamente no quieres que llegue a producción: logs con tokens, herramientas de inspección interna, datos de prueba. Con if ese código viaja al usuario aunque nunca se ejecute. Con #if DEBUG lo eliminas del binario por completo.
#if DEBUG
El más común. Swift lo activa cuando compilas en modo Debug — el default al hacer ⌘R en Xcode. En Release no existe.
#if DEBUG
print("[Auth] Token: \(token)")
#endif
Algo que me tomó tiempo entender cuando empecé a usarlo: DEBUG no lo defines tú, lo activa Xcode automáticamente según la configuración de build activa. Eso tiene una consecuencia importante: si en tu proyecto tienes una configuración custom como Staging, DEBUG no estará activa ahí a menos que vayas explícitamente a Build Settings → Swift Active Compilation Conditions y la agregues. Más de una vez me quedé sin logs en staging sin saber por qué.
#if DEBUG vs assert()
Son herramientas distintas aunque a veces cubran casos parecidos, y vale la pena entender cuándo usar cada una.
assert() evalúa una condición en runtime y crashea si es falsa. En release el compilador elimina el assert — pero los argumentos que le pasas sí compilan, y si tienen efectos secundarios pueden ejecutarse sin que el assert dispare. Es fácil meter un bug ahí sin darse cuenta.
assertionFailure() es lo mismo pero sin condición: crashea siempre que se alcanza. Lo usamos en el @UserDefault del post de feature flags para avisar de un type mismatch — si el compilador llega a esa línea en debug, es un error que hay que corregir.
#if DEBUG es más radical que los dos: el código dentro no compila en release, punto. Lo uso cuando el bloque entero no tiene sentido fuera de desarrollo — no como guardia contra errores, sino para que literalmente no exista en el binario final.
// Herramienta de debug que no debería existir en producción
#if DEBUG
func dumpFullState() -> String { ... }
#endif
// Verificar una invariante que debe cumplirse siempre en desarrollo
assert(index < array.count, "índice fuera de rango")
// Camino que nunca debería alcanzarse
assertionFailure("tipo no manejado: \(type)")
Checks de plataforma
Cuando el mismo código necesita correr en más de una plataforma, #if os() y #if canImport() se vuelven esenciales. El caso más típico: definir tipos o importar frameworks distintos según el sistema.
#if os(macOS)
import AppKit
typealias PlatformColor = NSColor
#elseif os(iOS)
import UIKit
typealias PlatformColor = UIColor
#endif
canImport hace algo más preciso — en vez de preguntar por el sistema operativo, pregunta directamente si un framework específico está disponible en el target:
#if canImport(AppKit)
// código específico de AppKit
#endif
Para código compartido entre múltiples targets, canImport tiende a ser más confiable que os(). No asume qué tiene cada plataforma — lo verifica directamente.
#if targetEnvironment(simulator)
Esta condición es verdadera cuando el código corre en el simulador de iOS o iPadOS. Algunos APIs de hardware simplemente no funcionan ahí — la cámara, ciertos sensores, algunas capacidades de CoreBluetooth. En esos casos, en vez de dejar que el código falle, prefiero sustituirlo explícitamente:
#if targetEnvironment(simulator)
// datos mock o UI placeholder
let photoData = UIImage(named: "mock-photo")
#else
// acceso real a cámara o hardware
startCameraSession()
#endif
Flags propios
Puedes definir los tuyos en Xcode: Build Settings → Swift Active Compilation Conditions. Yo los configuro por configuración de build:
Debug: DEBUG ANALYTICS_DISABLED
Staging: STAGING
Release: (vacío)
En código:
#if STAGING
let apiURL = "https://staging.api.tuapp.com"
#else
let apiURL = "https://api.tuapp.com"
#endif
La ventaja sobre un if con una constante es que el compilador elimina el código que no aplica. En release, la URL de staging no existe ni como string en el binario — no hay forma de que alguien la encuentre con un analizador de binarios.
Combinarlos
Puedes usar &&, || y ! para combinar condiciones. Lo uso seguido para acotar checks a una plataforma y configuración específicas:
#if DEBUG && os(macOS)
// solo en debug builds de macOS
#endif
#if !DEBUG
// cualquier configuración que no sea debug
#endif
Cuándo usarlo y cuándo no
La compilación condicional es una herramienta de eliminación, no de organización. Si la usas para estructurar lógica de negocio, el código se vuelve difícil de seguir rápidamente — leer un archivo lleno de #if es agotador.
Tiene sentido usarla cuando el código en cuestión no debería existir en ciertos contextos: herramientas de debug, código específico de plataforma, configuración que cambia entre entornos. No tanto para bifurcar comportamiento dentro de la misma función de negocio.
Una regla que me funciona: si borraras el bloque #if y el código que está adentro, ¿la app seguiría siendo correcta en ese contexto? Si la respuesta es sí, el #if tiene sentido. Si no, probablemente es lógica condicional que debería vivir en runtime.