Conditional Compilation permite ao compilador incluir ou pular partes do código-fonte dependendo de condições conhecidas no momento da compilação. De acordo com The Swift Programming Language (2026), a diretiva #if é processada durante a análise AST antes da geração de código de máquina. Conditional Compilation oferece aos desenvolvedores a capacidade de manter uma base de código única para múltiplas plataformas e configurações sem duplicação.
Principais pontos
Conditional Compilation é um mecanismo onde o compilador analisa as diretivas de compilação condicional e inclui no binário de saída apenas os blocos de código cujas condições são atendidas. Isso permite ter uma única base de código que se adapta a diferentes plataformas e configurações alvo.
O conceito veio de C/C++ com as diretivas de pré-processador #ifdef, #ifndef, #endif. Em linguagens modernas (Swift, Rust, Go), o mecanismo funciona no nível do compilador sem um pré-processador separado, o que aumenta a segurança: blocos condicionais devem ser sintaticamente corretos mesmo se não forem compilados.
De acordo com a sessão da Apple WWDC “Embrace Swift” (2025), cerca de 40% dos projetos Swift usam compilação condicional para suportar iOS e macOS em um único alvo. Para projetos com UIKit e SwiftUI, o código de UI é frequentemente dividido por diretivas #if os(iOS) e #if os(macOS), permitindo reutilizar a lógica de negócios.
A principal vantagem é a segurança em tempo de compilação. O código para uma plataforma inadequada não apenas não é executado, mas não é compilado. Isso significa que erros no código específico do iOS não aparecerão ao compilar para macOS, e vice-versa. Verificações em tempo de execução não oferecem tais garantias.
Swift fornece quatro diretivas principais: #if, #elseif, #else, #endif. Diferente do pré-processador C, Swift exige correção sintática do código em todos os ramos — o compilador analisa todo o código, mas gera código de máquina apenas para os ramos ativos.
Swift suporta funções de verificação integradas: os(iOS), os(macOS), os(tvOS), os(watchOS), os(Linux), os(Windows). Essas funções verificam a plataforma alvo para a qual o aplicativo está sendo compilado. A combinação com && e || permite criar condições complexas.
// Código unificado para iOS, macOS e 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 suporta verificação de versão do compilador: #if swift(>=5.9). Isso é útil para bibliotecas e frameworks que suportam múltiplas versões do Swift. Novos recursos da linguagem (como macros no Swift 5.9) podem ser protegidos por tal verificação.
// Compatibilidade retroativa
#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
A função canImport(ModuleName) verifica se o módulo especificado está disponível no ambiente de compilação atual. Este é o mecanismo mais flexível: não está vinculado a uma plataforma específica. Por exemplo, código usando CoreHaptics será compilado apenas em dispositivos onde este framework estiver disponível.
#if canImport(CoreHaptics)
import CoreHaptics
class HapticManager {
private var engine: CHHapticEngine?
func playTapFeedback() {
guard let engine else { return }
// Implementação de resposta háptica
}
}
#endif
Kotlin como linguagem não possui diretivas de pré-processador. Em vez disso, o ecossistema Android oferece três alternativas: campos BuildConfig (verificações em tempo de execução), sourceSets (substituição de arquivos inteiros) e expect/actual (no Kotlin Multiplatform).
Os sourceSets do Gradle permitem ter diferentes implementações de classes para diferentes flavors ou tipos de compilação. No diretório src/debug/ está a implementação para debug, em src/release/ — para release. Ao compilar, o Gradle seleciona o sourceSet apropriado e compila apenas seus arquivos.
// 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 no release
}
}
O KMP fornece o mecanismo expect (declaração no código comum) e actual (implementação para uma plataforma específica). É um mecanismo em tempo de compilação: para iOS, a implementação actual do sourceSet iOS é compilada; para Android — do sourceSet Android. Implementações não alvo não são compiladas.
// commonMain — declaração 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
Ao desenvolver bibliotecas nativas através do Android NDK, o pré-processador clássico C/C++ com as diretivas #ifdef, #ifndef, #define é usado. Diferente do Swift, o pré-processador C funciona no nível de texto — o código em ramos inativos pode ser sintaticamente incorreto.
O NDK define macros para cada plataforma: __ANDROID__ (Android), __APPLE__ (iOS/macOS), __linux__ (Linux). Para arquiteturas: __arm__, __aarch64__, __x86_64__. Esses macros são definidos pelo compilador automaticamente ao compilar para a plataforma alvo.
// 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
Ao trabalhar com NDK, é importante lembrar que o pré-processador C/C++ é uma substituição de texto. Se houver um erro sintático em um ramo inativo, o compilador não o verá, mas se um #define incorreto quebrar um ramo ativo — o erro aparecerá. Recomenda-se minimizar cadeias de #define e usar constantes constexpr.
Para Rust, que também é usado no desenvolvimento móvel através de UniFFI e Mozilla Application Services, existe seu próprio mecanismo — feature flags no Cargo.toml. Flags como #[cfg(target_os = "android")] em Rust funcionam de maneira similar às diretivas Swift: a verificação é feita no nível do compilador, não do pré-processador. Isso torna Rust uma escolha atraente para bibliotecas nativas que devem compilar para Android e iOS a partir de uma única base de código.
Conditional Compilation é eficaz em cenários estritamente definidos. Quando usada incorretamente, cria código com cheiro (code smell) difícil de testar e manter. Vamos considerar os cenários corretos e erros típicos.
O primeiro cenário é a abstração de plataforma: uma única Fachada com Conditional Compilation selecionando a implementação de plataforma internamente. O segundo é a depuração e criação de perfil: ferramentas de desenvolvimento que não devem ir para o lançamento. O terceiro é a compatibilidade retroativa: suporte para versões antigas do SO até que a versão mínima seja atualizada.
| Cenário | Linguagem | Condição |
|---|---|---|
| Abstração de plataforma | Swift | #if os(iOS) |
| Depuração | Swift/ObjC | #if DEBUG |
| Compatibilidade retroativa | Swift | #if swift(>=5.7) |
| Biblioteca nativa | C/C++ | #ifdef __ANDROID__ |
| Teste A/B | Java/Kotlin | BuildConfig.FLAVOR |
O antipadrão mais perigoso é a proliferação de diretivas por todo o código. Se cada segundo arquivo contém #if, é um sinal de que a arquitetura precisa de refatoração. A solução correta é extrair o código de plataforma para trás de protocolos/interfaces e usar Injeção de Dependência.
Perguntas frequentes
Conditional Compilation funciona em tempo de compilação: código inativo não vai para o binário. Verificações em tempo de execução (if / switch) são sempre compiladas, a condição é verificada durante a execução. O primeiro é mais seguro e eficiente, o segundo é mais flexível (pode ser alterado sem recompilar).
Sim, Swift permite diretivas #if dentro de funções, loops e até dentro de expressões. Este é um dos recursos que faltavam nas primeiras versões do Swift. Por exemplo: let x = #if DEBUG 1 #else 0 #endif — código válido.
Os desenvolvedores do Kotlin recusaram deliberadamente o pré-processador, considerando-o uma fonte de código frágil. Em vez disso, eles oferecem expect/actual (segurança em tempo de compilação) e sourceSets do Gradle (isolamento no nível de arquivos). Ambas as abordagens são mais confiáveis que a substituição de texto.
Compile o aplicativo com diferentes combinações de flags no CI. Para Swift: configure esquemas Xcode separados com diferentes Active Compilation Conditions. Para Android: configure Build Variants separados e execute testes para cada um. A automação é obrigatória.
Em Swift, a condição #if é uma diretiva do compilador. Se a própria condição for sintaticamente incorreta (por exemplo, um erro de digitação no nome os()), o compilador emitirá um erro de compilação. Em C/C++, o pré-processador simplesmente não encontrará o macro e a condição se tornará falsa.
Resumo
Vamos desenvolver um aplicativo móvel chave na mão
A IT Sectr cria aplicativos para iOS e Android para startups e empresas desde 2017. Nós vamos aconselhá-lo e propor a melhor solução.
Leia também