Conditional Compilation en aplicaciones móviles — esencia, directivas y principio de funcionamiento

Autor: IT Sectr Publicado: 2026-06-01 Tiempo de lectura: 9 min

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 — técnica de compilación selectiva de código según condiciones de plataforma, configuración o versión del lenguaje.
  • Directivas #if, #elseif, #else, #endif — las principales construcciones de compilación condicional en Swift, C, C++, Objective-C.
  • Kotlin no tiene directivas de preprocesador — en su lugar se usan BuildConfig, expect/actual y sourceSets.
  • Ventaja — el código para plataformas no adecuadas no se compila, reduciendo el tamaño del binario y eliminando errores.
  • iOS/macOS código compartido — Conditional Compilation es la base del desarrollo de frameworks multiplataforma de Apple.

Qué es Conditional Compilation

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.

Conditional Compilation en Swift

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.

Comprobaciones de plataforma os()

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.

swift
// 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
    }
}

Comprobaciones de versión del compilador

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.

swift
// 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

Comprobación de disponibilidad de módulo canImport()

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.

swift
#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

Alternativas en Kotlin y Android

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).

Source Sets en Gradle

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.

kotlin
// 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
    }
}

Expect/Actual en Kotlin Multiplatform

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.

kotlin
// 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

Preprocesador C/C++ y NDK

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.

Flags de plataforma NDK

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.

cpp
// 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.

Escenarios prácticos y antipatrones

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.

Escenarios correctos

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.

EscenarioLenguajeCondición
Abstracción de plataformaSwift#if os(iOS)
DepuraciónSwift/ObjC#if DEBUG
Compatibilidad hacia atrásSwift#if swift(>=5.7)
Biblioteca nativaC/C++#ifdef __ANDROID__
Pruebas A/BJava/KotlinBuildConfig.FLAVOR

Antipatrones

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.

  • #if en cada archivo — un antipatrón arquitectónico. El código de plataforma debe estar aislado detrás de protocolos.
  • #if anidados — rápidamente se vuelven ilegibles. La profundidad de anidamiento no debe superar los 2 niveles.
  • Duplicar funciones enteras — si una función está completamente copiada en #if y #else, debe extraerse a una parte común.
  • Pruebas — el código dentro de ramas inactivas no se prueba. Son necesarias compilaciones CI de todas las combinaciones posibles.
  • Flags mágicos — flags no documentados que el nuevo equipo de desarrollo desconoce.

Preguntas frecuentes

¿En qué se diferencia Conditional Compilation de las comprobaciones en tiempo de ejecución?

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).

¿Se puede usar #if dentro de una función en Swift?

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.

¿Por qué Kotlin no añadió un preprocesador?

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.

¿Cómo probar el código dentro de ramas #if inactivas?

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.

¿Qué sucede si la condición #if contiene un error sintáctico?

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

  • Conditional Compilation — técnica de compilación que excluye código no objetivo en tiempo de compilación, a diferencia de las comprobaciones en tiempo de ejecución.
  • Swift admite #if con os(), canImport(), swift() — directivas seguras en tiempo de compilación que requieren corrección sintáctica de todas las ramas.
  • Kotlin usa expect/actual y sourceSets de Gradle en lugar de un preprocesador — enfoques más fiables pero menos flexibles.
  • C/C++ en NDK usa el preprocesador de texto clásico #ifdef / #ifndef con macros de plataforma __ANDROID__, __APPLE__.
  • Uso correcto — abstracción de plataforma, depuración, compatibilidad hacia atrás. Uso incorrecto — #if en cada archivo, anidamiento profundo, flags mágicos.
  • CI es obligatorio — todas las combinaciones de flags deben compilarse y probarse automáticamente, de lo contrario el código en ramas inactivas se vuelve muerto.

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.

Discutir el proyecto

Lea también