La Conditional Compilation permet au compilateur d'inclure ou d'ignorer des parties du code source en fonction de conditions connues au moment de la compilation. Selon The Swift Programming Language (2026), la directive #if est traitée lors de l'analyse AST avant la génération de code machine. Conditional Compilation offre aux développeurs la capacité de maintenir une base de code unique pour plusieurs plates-formes et configurations sans duplication.
Points clés
Conditional Compilation est un mécanisme par lequel le compilateur analyse les directives de compilation conditionnelle et n'inclut dans le binaire de sortie que les blocs de code dont les conditions sont remplies. Cela permet d'avoir une base de code unique qui s'adapte à différentes plates-formes et configurations cibles.
Le concept vient du C/C++ avec les directives de préprocesseur #ifdef, #ifndef, #endif. Dans les langages modernes (Swift, Rust, Go), le mécanisme fonctionne au niveau du compilateur sans préprocesseur séparé, ce qui augmente la sécurité : les blocs conditionnels doivent être syntaxiquement corrects même s'ils ne sont pas compilés.
Selon la session Apple WWDC « Embrace Swift » (2025), environ 40% des projets Swift utilisent la compilation conditionnelle pour prendre en charge iOS et macOS dans une seule cible. Pour les projets avec UIKit et SwiftUI, le code de l'interface utilisateur est souvent divisé par des directives #if os(iOS) et #if os(macOS), permettant la réutilisation de la logique métier.
Le principal avantage est la sécurité au moment de la compilation. Le code pour une plateforme inappropriée n'est pas seulement non exécuté, mais non compilé. Cela signifie que les erreurs dans le code spécifique à iOS n'apparaîtront pas lors de la compilation pour macOS, et vice versa. Les vérifications à l'exécution n'offrent pas de telles garanties.
Swift fournit quatre directives clés : #if, #elseif, #else, #endif. Contrairement au préprocesseur C, Swift exige la correction syntaxique du code dans toutes les branches — le compilateur analyse tout le code mais génère du code machine uniquement pour les branches actives.
Swift prend en charge des fonctions de vérification intégrées : os(iOS), os(macOS), os(tvOS), os(watchOS), os(Linux), os(Windows). Ces fonctions vérifient la plateforme cible pour laquelle l'application est compilée. La combinaison avec && et || permet de créer des conditions complexes.
// Code unifié pour iOS, macOS et 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 prend en charge la vérification de version du compilateur : #if swift(>=5.9). Ceci est utile pour les bibliothèques et frameworks qui supportent plusieurs versions de Swift. Les nouvelles fonctionnalités du langage (comme les macros dans Swift 5.9) peuvent être protégées par une telle vérification.
// Compatibilité ascendante
#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 fonction canImport(ModuleName) vérifie si le module spécifié est disponible dans l'environnement de compilation actuel. C'est le mécanisme le plus flexible : il n'est lié à aucune plateforme spécifique. Par exemple, le code utilisant CoreHaptics ne sera compilé que sur les appareils où ce framework est disponible.
#if canImport(CoreHaptics)
import CoreHaptics
class HapticManager {
private var engine: CHHapticEngine?
func playTapFeedback() {
guard let engine else { return }
// Implémentation du retour haptique
}
}
#endif
Kotlin en tant que langage n'a pas de directives de préprocesseur. À la place, l'écosystème Android offre trois alternatives : les champs BuildConfig (vérifications à l'exécution), sourceSets (remplacement de fichiers entiers) et expect/actual (dans Kotlin Multiplatform).
Les sourceSets de Gradle permettent d'avoir différentes implémentations de classes pour différents flavors ou types de compilation. Dans le répertoire src/debug/ se trouve l'implémentation de débogage, dans src/release/ — l'implémentation de release. Lors de la compilation, Gradle sélectionne le sourceSet approprié et ne compile que ses fichiers.
// 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 fournit le mécanisme expect (déclaration dans le code commun) et actual (implémentation pour une plateforme spécifique). C'est un mécanisme au moment de la compilation : pour iOS, l'implémentation actual du sourceSet iOS est compilée ; pour Android — du sourceSet Android. Les implémentations non ciblées ne sont pas compilées.
// commonMain — déclaration expect
expect fun getPlatformName(): String
// androidMain — actual pour Android
actual fun getPlatformName(): String =
"Android \${Build.VERSION.SDK_INT}"
// iosMain — actual pour iOS
actual fun getPlatformName(): String =
UIDevice.current.systemName() + " " + UIDevice.current.systemVersion
Lors du développement de bibliothèques natives via Android NDK, le préprocesseur classique C/C++ avec les directives #ifdef, #ifndef, #define est utilisé. Contrairement à Swift, le préprocesseur C fonctionne au niveau du texte — le code dans les branches inactives peut être syntaxiquement incorrect.
NDK définit des macros pour chaque plateforme : __ANDROID__ (Android), __APPLE__ (iOS/macOS), __linux__ (Linux). Pour les architectures : __arm__, __aarch64__, __x86_64__. Ces macros sont définies automatiquement par le compilateur lors de la compilation pour la plateforme cible.
// Code natif pour Android et 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
Lorsqu'on travaille avec NDK, il est important de se rappeler que le préprocesseur C/C++ est un remplacement de texte. S'il y a une erreur syntaxique dans une branche inactive, le compilateur ne la verra pas, mais si un #define incorrect casse une branche active — l'erreur apparaîtra. Il est recommandé de minimiser les chaînes de #define et d'utiliser des constantes constexpr.
Pour Rust, qui est également utilisé dans le développement mobile via UniFFI et Mozilla Application Services, il existe son propre mécanisme — les feature flags dans Cargo.toml. Les drapeaux comme #[cfg(target_os = « android »)] dans Rust fonctionnent de manière similaire aux directives Swift : la vérification est effectuée au niveau du compilateur, pas du préprocesseur. Cela fait de Rust un choix attrayant pour les bibliothèques natives qui doivent être compilées pour Android et iOS à partir d'une seule base de code.
Conditional Compilation est efficace dans des scénarios strictement définis. Lorsqu'elle est utilisée incorrectement, elle crée un code smell difficile à tester et à maintenir. Examinons les scénarios corrects et les erreurs typiques.
Le premier scénario est l'abstraction de plateforme : une seule Façade avec Conditional Compilation sélectionnant l'implémentation de plateforme en interne. Le second est le débogage et le profilage : des outils de développement qui ne doivent pas se retrouver dans la release. Le troisième est la compatibilité ascendante : la prise en charge des anciennes versions d'OS jusqu'à ce que la version minimale soit mise à jour.
| Scénario | Langage | Condition |
|---|---|---|
| Abstraction de plateforme | Swift | #if os(iOS) |
| Débogage | Swift/ObjC | #if DEBUG |
| Compatibilité ascendante | Swift | #if swift(>=5.7) |
| Bibliothèque native | C/C++ | #ifdef __ANDROID__ |
| Test A/B | Java/Kotlin | BuildConfig.FLAVOR |
L'antipattern le plus dangereux est la prolifération des directives dans tout le code. Si un fichier sur deux contient #if, c'est un signe que l'architecture a besoin d'être refactorisée. La bonne solution est d'extraire le code de plateforme derrière des protocoles/interfaces et d'utiliser l'Injection de Dépendances.
Foire aux questions
Conditional Compilation fonctionne au moment de la compilation : le code inactif ne se retrouve pas dans le binaire. Les vérifications à l'exécution (if / switch) sont toujours compilées, la condition est vérifiée pendant l'exécution. La première est plus sûre et efficace, la seconde est plus flexible (peut être modifiée sans reconstruire).
Oui, Swift permet les directives #if à l'intérieur des fonctions, des boucles et même à l'intérieur des expressions. C'est l'une des fonctionnalités qui manquait dans les premières versions de Swift. Par exemple : let x = #if DEBUG 1 #else 0 #endif — code valide.
Les développeurs de Kotlin ont délibérément refusé le préprocesseur, le considérant comme une source de code fragile. À la place, ils proposent expect/actual (sécurité au moment de la compilation) et les sourceSets de Gradle (isolation au niveau des fichiers). Les deux approches sont plus fiables que le remplacement de texte.
Compilez l'application avec différentes combinaisons de drapeaux dans le CI. Pour Swift : configurez des schémas Xcode séparés avec différentes Active Compilation Conditions. Pour Android : configurez des Build Variants séparés et exécutez des tests pour chacun. L'automatisation est obligatoire.
Dans Swift, la condition #if est une directive du compilateur. Si la condition elle-même est syntaxiquement incorrecte (par exemple, une faute de frappe dans le nom os()), le compilateur émettra une erreur de compilation. En C/C++, le préprocesseur ne trouvera tout simplement pas la macro et la condition deviendra fausse.
Résumé
Nous développerons une application mobile clé en main
IT Sectr crée des applications iOS et Android pour les startups et les entreprises depuis 2017. Nous vous conseillerons et vous proposerons la meilleure solution.
Lisez aussi