Conditional Compilation permite al compilador incluir u omitir partes del código fuente dependiendo de condiciones conocidas en tiempo de compilación. Según The Swift Programming Language (2026), la directiva #if se procesa durante el análisis AST antes de la generación de código máquina. Conditional Compilation brinda a los desarrolladores la capacidad de mantener una base de código única para múltiples plataformas y configuraciones sin duplicación.
Puntos clave
Conditional Compilation es un mecanismo mediante el cual el compilador analiza las directivas de compilación condicional e incluye en el binario de salida solo aquellos bloques de código cuyas condiciones se cumplen. Esto permite tener una base de código única que se adapta a diferentes plataformas y configuraciones objetivo.
El concepto proviene de C/C++ con las directivas de preprocesador #ifdef, #ifndef, #endif. En los lenguajes modernos (Swift, Rust, Go), el mecanismo funciona a nivel del compilador sin un preprocesador separado, lo que aumenta la seguridad: los bloques condicionales deben ser sintácticamente correctos incluso si no se compilan.
Según la sesión de Apple WWDC "Embrace Swift" (2025), alrededor del 40% de los proyectos Swift utilizan compilación condicional para soportar iOS y macOS en un único target. Para proyectos con UIKit y SwiftUI, el código de UI a menudo se divide con directivas #if os(iOS) y #if os(macOS), permitiendo reutilizar la lógica de negocio.
La principal ventaja es la seguridad en tiempo de compilación. El código para una plataforma no adecuada no solo no se ejecuta, sino que no se compila. Esto significa que los errores en el código específico de iOS no aparecerán al compilar para macOS, y viceversa. Las comprobaciones en tiempo de ejecución no ofrecen tales garantías.
Swift proporciona cuatro directivas clave: #if, #elseif, #else, #endif. A diferencia del preprocesador de C, Swift exige corrección sintáctica del código en todas las ramas — el compilador analiza todo el código pero genera código máquina solo para las ramas activas.
Swift admite funciones de comprobación integradas: os(iOS), os(macOS), os(tvOS), os(watchOS), os(Linux), os(Windows). Estas funciones verifican la plataforma objetivo para la que se compila la aplicación. La combinación con && y || permite crear condiciones complejas.
// Código único para iOS, macOS y tvOS
import Foundation
class PlatformService {
func getSystemVersion() -> String {
#if os(iOS) || os(tvOS)
return UIDevice.current.systemVersion
#elseif os(macOS)
let vers = ProcessInfo.processInfo.operatingSystemVersion
return "\(vers.majorVersion).\(vers.minorVersion)"
#else
return "unknown"
#endif
}
}
Swift admite la comprobación de la versión del compilador: #if swift(>=5.9). Esto es útil para bibliotecas y frameworks que soportan múltiples versiones de Swift. Las nuevas características del lenguaje (como las macros en Swift 5.9) pueden protegerse con dicha comprobación.
// Compatibilidad hacia atrás
#if swift(>=5.9)
@MainActor
struct ModernView: View {
var body: some View {
Text("Modern SwiftUI")
}
}
#else
struct ModernView: View {
var body: some View {
Text("Legacy SwiftUI")
}
}
#endif
La función canImport(ModuleName) verifica si el módulo especificado está disponible en el entorno de compilación actual. Este es el mecanismo más flexible: no está vinculado a una plataforma específica. Por ejemplo, el código que usa CoreHaptics solo se compilará en dispositivos donde este framework esté disponible.
#if canImport(CoreHaptics)
import CoreHaptics
class HapticManager {
private var engine: CHHapticEngine?
func playTapFeedback() {
guard let engine else { return }
// Implementación de respuesta háptica
}
}
#endif
Kotlin como lenguaje no tiene directivas de preprocesador. En su lugar, el ecosistema Android ofrece tres alternativas: campos BuildConfig (comprobaciones en tiempo de ejecución), sourceSets (reemplazo de archivos completos) y expect/actual (en Kotlin Multiplatform).
Los sourceSets de Gradle permiten tener diferentes implementaciones de clases para diferentes flavors o tipos de compilación. En el directorio src/debug/ se encuentra la implementación para debug, en src/release/ — para release. Al compilar, Gradle selecciona el sourceSet correspondiente y compila solo sus archivos.
// src/debug/kotlin/com/example/Logger.kt
object Logger {
fun log(tag: String, message: String) {
Log.d(tag, message)
}
}
// src/release/kotlin/com/example/Logger.kt
object Logger {
fun log(tag: String, message: String) {
// No-op en release
}
}
KMP proporciona el mecanismo expect (declaración en código común) y actual (implementación para una plataforma específica). Es un mecanismo en tiempo de compilación: para iOS se compila la implementación actual del sourceSet de iOS, para Android — del sourceSet de Android. Las implementaciones no objetivo no se compilan.
// commonMain — declaración expect
expect fun getPlatformName(): String
// androidMain — actual para Android
actual fun getPlatformName(): String =
"Android \${Build.VERSION.SDK_INT}"
// iosMain — actual para iOS
actual fun getPlatformName(): String =
UIDevice.current.systemName() + " " + UIDevice.current.systemVersion
Al desarrollar bibliotecas nativas a través de Android NDK, se utiliza el preprocesador clásico de C/C++ con las directivas #ifdef, #ifndef, #define. A diferencia de Swift, el preprocesador de C funciona a nivel de texto — el código en ramas inactivas puede ser sintácticamente incorrecto.
NDK define macros para cada plataforma: __ANDROID__ (Android), __APPLE__ (iOS/macOS), __linux__ (Linux). Para arquitecturas: __arm__, __aarch64__, __x86_64__. Estas macros las establece el compilador automáticamente al compilar para la plataforma objetivo.
// Código nativo para Android e iOS
#include <cstdint>
#ifdef __ANDROID__
int32_t getJniEnv(JNIEnv* env) {
return env->GetVersion();
}
#elif defined(__APPLE__)
#include <TargetConditionals.h>
int32_t getOsVersion() {
#if TARGET_OS_IOS
return "iOS";
#elif TARGET_OS_OSX
return "macOS";
#endif
}
#endif
Al trabajar con NDK, es importante recordar que el preprocesador de C/C++ es un reemplazo de texto. Si hay un error sintáctico en una rama inactiva, el compilador no lo verá, pero si un #define incorrecto rompe una rama activa — el error aparecerá. Se recomienda minimizar las cadenas de #define y usar constantes constexpr.
Para Rust, que también se usa en desarrollo móvil a través de UniFFI y Mozilla Application Services, existe su propio mecanismo — feature flags en Cargo.toml. Flags como #[cfg(target_os = "android")] en Rust funcionan de manera similar a las directivas de Swift: la comprobación se realiza a nivel del compilador, no del preprocesador. Esto hace de Rust una opción atractiva para bibliotecas nativas que deben compilarse para Android e iOS desde una única base de código.
Conditional Compilation es efectiva en escenarios estrictamente definidos. Cuando se usa incorrectamente, crea código con olor (code smell) difícil de probar y mantener. Veamos los escenarios correctos y los errores típicos.
El primer escenario es la abstracción de plataforma: una única Fachada con Conditional Compilation seleccionando la implementación de plataforma internamente. El segundo es la depuración y creación de perfiles: herramientas de desarrollo que no deben llegar a la versión de lanzamiento. El tercero es la compatibilidad hacia atrás: soporte para versiones antiguas del SO hasta que se actualice la versión mínima.
| Escenario | Lenguaje | Condición |
|---|---|---|
| Abstracción de plataforma | Swift | #if os(iOS) |
| Depuración | Swift/ObjC | #if DEBUG |
| Compatibilidad hacia atrás | Swift | #if swift(>=5.7) |
| Biblioteca nativa | C/C++ | #ifdef __ANDROID__ |
| Pruebas A/B | Java/Kotlin | BuildConfig.FLAVOR |
El antipatrón más peligroso es la proliferación de directivas por todo el código. Si cada dos archivos contiene #if, es una señal de que la arquitectura necesita refactorización. La solución correcta es extraer el código de plataforma detrás de protocolos/interfaces y usar Inyección de Dependencias.
Preguntas frecuentes
Conditional Compilation funciona en tiempo de compilación: el código inactivo no llega al binario. Las comprobaciones en tiempo de ejecución (if / switch) siempre se compilan, la condición se verifica durante la ejecución. Lo primero es más seguro y eficiente, lo segundo es más flexible (se puede cambiar sin recompilar).
Sí, Swift permite directivas #if dentro de funciones, bucles e incluso dentro de expresiones. Esta es una de las características que faltaban en las primeras versiones de Swift. Por ejemplo: let x = #if DEBUG 1 #else 0 #endif — código válido.
Los desarrolladores de Kotlin rechazaron deliberadamente el preprocesador, considerándolo una fuente de código frágil. En su lugar, ofrecen expect/actual (seguridad en tiempo de compilación) y sourceSets de Gradle (aislamiento a nivel de archivos). Ambos enfoques son más fiables que el reemplazo de texto.
Compile la aplicación con diferentes combinaciones de flags en CI. Para Swift: configure esquemas de Xcode separados con diferentes Active Compilation Conditions. Para Android: configure Build Variants separados y ejecute pruebas para cada uno. La automatización es obligatoria.
En Swift, la condición #if es una directiva del compilador. Si la condición en sí es sintácticamente incorrecta (por ejemplo, un error tipográfico en el nombre de os()), el compilador emitirá un error de compilación. En C/C++, el preprocesador simplemente no encontrará la macro y la condición se volverá falsa.
Resumen
Desarrollaremos una aplicación móvil llave en mano
IT Sectr crea aplicaciones para iOS y Android para startups y empresas desde 2017. Le asesoraremos y le propondremos la mejor solución.
Lea también