Conditional Compilation in mobilen Apps — Wesen, Direktiven und Funktionsprinzip

Autor: IT Sectr Veröffentlicht: 2026-06-01 Lesezeit: 9 Min.

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 — Technik zur selektiven Kompilierung von Code nach Plattform-, Konfigurations- oder Sprachversionsbedingungen.
  • Direktiven #if, #elseif, #else, #endif — die wichtigsten Konstrukte der bedingten Kompilierung in Swift, C, C++, Objective-C.
  • Kotlin hat keine Präprozessordirektiven — stattdessen werden BuildConfig, expect/actual und sourceSets verwendet.
  • Vorteil — Code für ungeeignete Plattformen wird nicht kompiliert, was die Binärgröße reduziert und Fehler eliminiert.
  • iOS/macOS gemeinsamer Code — Conditional Compilation ist die Grundlage der plattformübergreifenden Apple-Framework-Entwicklung.

Was ist Conditional Compilation

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.

Conditional Compilation in Swift

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.

Plattformprüfungen os()

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.

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

Compiler-Versionsprüfungen

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.

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

Modulverfügbarkeitsprüfung canImport()

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.

swift
#if canImport(CoreHaptics)
    import CoreHaptics

    class HapticManager {
        private var engine: CHHapticEngine?

        func playTapFeedback() {
            guard let engine else { return }
            // Implementierung des haptischen Feedbacks
        }
    }
#endif

Alternativen in Kotlin und Android

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

Source Sets in Gradle

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.

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 im Release
    }
}

Expect/Actual in Kotlin Multiplatform

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.

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

C/C++-Präprozessor und NDK

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-Plattformflags

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.

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

Praktische Szenarien und Antipatterns

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.

Korrekte Szenarien

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.

SzenarioSpracheBedingung
PlattformabstraktionSwift#if os(iOS)
DebuggingSwift/ObjC#if DEBUG
RückwärtskompatibilitätSwift#if swift(>=5.7)
Native BibliothekC/C++#ifdef __ANDROID__
A/B-TestsJava/KotlinBuildConfig.FLAVOR

Antipatterns

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.

  • #if in jeder Datei — ein architektonisches Antipattern. Plattformcode sollte hinter Protokollen isoliert werden.
  • Verschachteltes #if — wird schnell unlesbar. Die Verschachtelungstiefe sollte 2 Ebenen nicht überschreiten.
  • Duplizieren ganzer Funktionen — wenn eine Funktion vollständig in #if und #else kopiert ist, sollte sie in einen gemeinsamen Teil extrahiert werden.
  • Tests — Code in inaktiven Zweigen wird nicht getestet. CI-Builds aller möglichen Kombinationen sind notwendig.
  • Magische Flags — undokumentierte Flags, von denen das neue Entwicklerteam nichts weiß.

Häufig gestellte Fragen

Wie unterscheidet sich Conditional Compilation von Laufzeitprüfungen?

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

Kann man #if innerhalb einer Funktion in Swift verwenden?

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.

Warum hat Kotlin keinen Präprozessor hinzugefügt?

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.

Wie testet man Code in inaktiven #if-Zweigen?

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.

Was passiert, wenn die #if-Bedingung einen Syntaxfehler enthält?

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

  • Conditional Compilation — eine Kompilierungstechnik, die im Gegensatz zu Laufzeitprüfungen nicht-zielgerichteten Code zur Build-Zeit ausschließt.
  • Swift unterstützt #if mit os(), canImport(), swift() — compilierzeitsichere Direktiven, die syntaktische Korrektheit aller Zweige erfordern.
  • Kotlin verwendet expect/actual und Gradle-sourceSets anstelle eines Präprozessors — zuverlässigere, aber weniger flexible Ansätze.
  • C/C++ im NDK verwendet den klassischen Textpräprozessor #ifdef / #ifndef mit Plattformmakros __ANDROID__, __APPLE__.
  • Richtige Verwendung — Plattformabstraktion, Debugging, Rückwärtskompatibilität. Falsche Verwendung — #if in jeder Datei, tiefe Verschachtelung, magische Flags.
  • CI ist obligatorisch — alle Flag-Kombinationen müssen automatisch gebaut und getestet werden, sonst wird Code in inaktiven Zweigen tot.

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.

Projekt besprechen

Lesen Sie auch