Conditional Compilation ermöglicht es dem Compiler, Teile des Quellcodes in Abhängigkeit von Bedingungen, die zum Build-Zeitpunkt bekannt sind, einzufügen oder zu überspringen. Laut The Swift Programming Language (2026) wird die #if-Direktive während der AST-Analyse vor der Generierung von Maschinencode verarbeitet. Conditional Compilation gibt Entwicklern die Möglichkeit, eine einzige Codebasis für mehrere Plattformen und Konfigurationen ohne Duplizierung zu pflegen.
Wichtige Punkte
Conditional Compilation ist ein Mechanismus, bei dem der Compiler die Direktiven der bedingten Kompilierung analysiert und nur die Codeblöcke in die Ausgabebinärdatei aufnimmt, deren Bedingungen erfüllt sind. Dies ermöglicht eine einzige Codebasis, die sich an verschiedene Zielplattformen und Konfigurationen anpasst.
Das Konzept stammt aus C/C++ mit den Präprozessordirektiven #ifdef, #ifndef, #endif. In modernen Sprachen (Swift, Rust, Go) arbeitet der Mechanismus auf Compiler-Ebene ohne separaten Präprozessor, was die Sicherheit erhöht: Bedingte Blöcke müssen syntaktisch korrekt sein, auch wenn sie nicht kompiliert werden.
Laut der Apple WWDC-Session „Embrace Swift“ (2025) verwenden etwa 40% der Swift-Projekte bedingte Kompilierung, um iOS und macOS in einem einzigen Target zu unterstützen. Bei Projekten mit UIKit und SwiftUI wird der UI-Code oft durch #if os(iOS)- und #if os(macOS)-Direktiven getrennt, was die Wiederverwendung von Geschäftslogik ermöglicht.
Der Hauptvorteil ist die Compile-Zeit-Sicherheit. Code für eine ungeeignete Plattform wird nicht nur nicht ausgeführt, sondern nicht kompiliert. Das bedeutet, dass Fehler im iOS-spezifischen Code beim Erstellen für macOS nicht auftreten und umgekehrt. Laufzeitprüfungen bieten solche Garantien nicht.
Swift bietet vier wichtige Direktiven: #if, #elseif, #else, #endif. Im Gegensatz zum C-Präprozessor verlangt Swift syntaktische Korrektheit des Codes in allen Zweigen — der Compiler parst den gesamten Code, erzeugt aber nur für aktive Zweige Maschinencode.
Swift unterstützt integrierte Prüfungsfunktionen: os(iOS), os(macOS), os(tvOS), os(watchOS), os(Linux), os(Windows). Diese Funktionen überprüfen die Zielplattform, für die die Anwendung erstellt wird. Die Kombination mit && und || ermöglicht die Erstellung komplexer Bedingungen.
// Einheitlicher Code für iOS, macOS und 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 unterstützt die Überprüfung der Compiler-Version: #if swift(>=5.9). Dies ist nützlich für Bibliotheken und Frameworks, die mehrere Swift-Versionen unterstützen. Neue Sprachfunktionen (wie Makros in Swift 5.9) können durch eine solche Prüfung geschützt werden.
// Rückwärtskompatibilität
#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
Die Funktion canImport(ModuleName) prüft, ob das angegebene Modul in der aktuellen Build-Umgebung verfügbar ist. Dies ist der flexibelste Mechanismus: Er ist nicht an eine bestimmte Plattform gebunden. Beispielsweise wird Code, der CoreHaptics verwendet, nur auf Geräten kompiliert, auf denen dieses Framework verfügbar ist.
#if canImport(CoreHaptics)
import CoreHaptics
class HapticManager {
private var engine: CHHapticEngine?
func playTapFeedback() {
guard let engine else { return }
// Implementierung des haptischen Feedbacks
}
}
#endif
Kotlin als Sprache hat keine Präprozessordirektiven. Stattdessen bietet das Android-Ökosystem drei Alternativen: BuildConfig-Felder (Laufzeitprüfungen), sourceSets (Ersetzung ganzer Dateien) und expect/actual (in Kotlin Multiplatform).
Gradle-sourceSets ermöglichen unterschiedliche Klassenimplementierungen für verschiedene Flavors oder Build-Typen. Im Verzeichnis src/debug/ befindet sich die Debug-Implementierung, in src/release/ die Release-Implementierung. Beim Build wählt Gradle das entsprechende sourceSet aus und kompiliert nur dessen Dateien.
// 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 im Release
}
}
KMP bietet den expect-Mechanismus (Deklaration im gemeinsamen Code) und actual (Implementierung für eine bestimmte Plattform). Dies ist ein Compile-Zeit-Mechanismus: Für iOS wird die actual-Implementierung aus dem iOS-sourceSet kompiliert, für Android aus dem Android-sourceSet. Nicht-Ziel-Implementierungen werden nicht kompiliert.
// commonMain — expect-Deklaration
expect fun getPlatformName(): String
// androidMain — actual für Android
actual fun getPlatformName(): String =
"Android \${Build.VERSION.SDK_INT}"
// iosMain — actual für iOS
actual fun getPlatformName(): String =
UIDevice.current.systemName() + " " + UIDevice.current.systemVersion
Bei der Entwicklung nativer Bibliotheken über Android NDK wird der klassische C/C++-Präprozessor mit den Direktiven #ifdef, #ifndef, #define verwendet. Im Gegensatz zu Swift arbeitet der C-Präprozessor auf Textebene — Code in inaktiven Zweigen kann syntaktisch inkorrekt sein.
NDK definiert Makros für jede Plattform: __ANDROID__ (Android), __APPLE__ (iOS/macOS), __linux__ (Linux). Für Architekturen: __arm__, __aarch64__, __x86_64__. Diese Makros werden vom Compiler automatisch gesetzt, wenn für die Zielplattform gebaut wird.
// Nativer Code für Android und 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
Bei der Arbeit mit NDK ist es wichtig zu bedenken, dass der C/C++-Präprozessor eine Textersetzung ist. Wenn in einem inaktiven Zweig ein Syntaxfehler vorliegt, sieht der Compiler ihn nicht, aber wenn ein falsches #define einen aktiven Zweig beschädigt, tritt der Fehler auf. Es wird empfohlen, #define-Ketten zu minimieren und constexpr-Konstanten zu verwenden.
Für Rust, das auch in der mobilen Entwicklung über UniFFI und Mozilla Application Services verwendet wird, gibt es einen eigenen Mechanismus — Feature-Flags in Cargo.toml. Flags wie #[cfg(target_os = „android“)] in Rust funktionieren ähnlich wie Swift-Direktiven: Die Prüfung erfolgt auf Compiler-Ebene, nicht auf Präprozessor-Ebene. Dies macht Rust zu einer attraktiven Wahl für native Bibliotheken, die aus einer einzigen Codebasis für Android und iOS kompiliert werden müssen.
Conditional Compilation ist in streng definierten Szenarien effektiv. Bei falscher Verwendung entsteht Code Smell, der schwer zu testen und zu warten ist. Betrachten wir die korrekten Szenarien und typischen Fehler.
Das erste Szenario ist die Plattformabstraktion: eine einzige Fassade, in der Conditional Compilation die Plattformimplementierung auswählt. Das zweite ist Debugging und Profiling: Entwicklerwerkzeuge, die nicht in die Release gelangen sollten. Das dritte ist die Rückwärtskompatibilität: Unterstützung für ältere OS-Versionen, bis die Mindestversion aktualisiert wird.
| Szenario | Sprache | Bedingung |
|---|---|---|
| Plattformabstraktion | Swift | #if os(iOS) |
| Debugging | Swift/ObjC | #if DEBUG |
| Rückwärtskompatibilität | Swift | #if swift(>=5.7) |
| Native Bibliothek | C/C++ | #ifdef __ANDROID__ |
| A/B-Tests | Java/Kotlin | BuildConfig.FLAVOR |
Das gefährlichste Antipattern ist die Ausbreitung von Direktiven im gesamten Code. Wenn jede zweite Datei #if enthält, ist das ein Zeichen dafür, dass die Architektur überarbeitet werden muss. Die richtige Lösung ist, Plattformcode hinter Protokolle/Schnittstellen zu extrahieren und Dependency Injection zu verwenden.
Häufig gestellte Fragen
Conditional Compilation arbeitet zur Compile-Zeit: Inaktiver Code gelangt nicht in die Binärdatei. Laufzeitprüfungen (if / switch) werden immer kompiliert, die Bedingung wird während der Ausführung geprüft. Ersteres ist sicherer und effizienter, Letzteres ist flexibler (kann ohne Neubau geändert werden).
Ja, Swift erlaubt Direktiven #if innerhalb von Funktionen, Schleifen und sogar innerhalb von Ausdrücken. Dies ist eine der Funktionen, die in frühen Versionen von Swift fehlten. Zum Beispiel: let x = #if DEBUG 1 #else 0 #endif — gültiger Code.
Die Kotlin-Entwickler haben bewusst auf einen Präprozessor verzichtet, da sie ihn als Quelle für zerbrechlichen Code betrachten. Stattdessen bieten sie expect/actual (Compile-Zeit-Sicherheit) und Gradle-sourceSets (Isolation auf Dateiebene). Beide Ansätze sind zuverlässiger als Textersetzung.
Erstellen Sie die Anwendung mit verschiedenen Flag-Kombinationen im CI. Für Swift: Konfigurieren Sie separate Xcode-Schemata mit verschiedenen Active Compilation Conditions. Für Android: Konfigurieren Sie separate Build Variants und führen Sie Tests für jede aus. Automatisierung ist obligatorisch.
In Swift ist die #if-Bedingung eine Compiler-Direktive. Wenn die Bedingung selbst syntaktisch inkorrekt ist (z. B. ein Tippfehler im os()-Namen), gibt der Compiler einen Kompilierungsfehler aus. In C/C++ findet der Präprozessor das Makro einfach nicht und die Bedingung wird falsch.
Zusammenfassung
Wir entwickeln eine mobile Applikation schlüsselfertig
IT Sectr entwickelt seit 2017 iOS- und Android-Apps für Startups und Unternehmen. Wir beraten Sie und schlagen die beste Lösung vor.
Lesen Sie auch