Kompilacja warunkowa w aplikacjach mobilnych — istota, dyrektywy i zasada działania

Autor: IT Sectr Opublikowano: 2026-06-01 Czas czytania: 9 min

Kompilacja warunkowa pozwala kompilatorowi włączać lub pomijać fragmenty kodu źródłowego w zależności od warunków znanych na etapie budowania. Według The Swift Programming Language (2026), dyrektywa #if jest przetwarzana na etapie analizy AST przed generowaniem kodu maszynowego. Kompilacja warunkowa daje programistom możliwość utrzymywania jednolitej bazy kodu dla wielu platform i konfiguracji bez powielania.

Najważniejsze

  • Kompilacja warunkowa — technika selektywnej kompilacji kodu według warunków platformy, konfiguracji lub wersji języka.
  • Dyrektywy #if, #elseif, #else, #endif — podstawowe konstrukcje kompilacji warunkowej w Swift, C, C++, Objective-C.
  • Kotlin nie ma dyrektyw preprocesora — zamiast tego używa się BuildConfig, expect/actual i sourceSets.
  • Zaleta — kod dla nieodpowiednich platform nie jest kompilowany, co zmniejsza rozmiar pliku binarnego i eliminuje błędy.
  • iOS/macOS wspólny kod — kompilacja warunkowa to podstawa tworzenia wieloplatformowych frameworków Apple.

Czym jest kompilacja warunkowa

Kompilacja warunkowa — to mechanizm, w którym kompilator analizuje dyrektywy kompilacji warunkowej i włącza do wynikowego pliku binarnego tylko te bloki kodu, dla których warunki są spełnione. Pozwala to na posiadanie jednolitej bazy kodu, która dostosowuje się do różnych platform docelowych i konfiguracji.

Koncepcja pochodzi z C/C++ z dyrektywami preprocesora #ifdef, #ifndef, #endif. W nowoczesnych językach (Swift, Rust, Go) mechanizm działa na poziomie kompilatora bez osobnego preprocesora, co zwiększa bezpieczeństwo: bloki warunkowe muszą być składniowo poprawne, nawet jeśli nie są kompilowane.

Według danych Apple WWDC Session „Embrace Swift” (2025), około 40% projektów Swift używa kompilacji warunkowej do obsługi iOS i macOS w jednym targetcie. Dla projektów z UIKit i SwiftUI kod UI jest często rozdzielony dyrektywami #if os(iOS) i #if os(macOS), co pozwala na ponowne wykorzystanie logiki biznesowej.

Główna zaleta — bezpieczeństwo na etapie kompilacji. Kod dla nieodpowiedniej platformy nie tylko nie jest wykonywany, ale nie jest kompilowany. Oznacza to, że błędy w kodzie specyficznym dla iOS nie ujawnią się podczas budowania dla macOS i odwrotnie. Sprawdzanie w czasie wykonywania nie daje takiej gwarancji.

Kompilacja warunkowa w Swift

Swift udostępnia cztery kluczowe dyrektywy: #if, #elseif, #else, #endif. W przeciwieństwie do preprocesora C, Swift wymaga poprawności składniowej kodu we wszystkich gałęziach — kompilator parsuje cały kod, ale generuje kod maszynowy tylko dla aktywnych gałęzi.

Sprawdzanie platformy os()

Swift obsługuje wbudowane funkcje sprawdzania: os(iOS), os(macOS), os(tvOS), os(watchOS), os(Linux), os(Windows). Te funkcje sprawdzają platformę docelową, dla której budowana jest aplikacja. Kombinacja z && i || pozwala tworzyć złożone warunki.

swift
// Wspólny kod dla iOS, macOS i 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
    }
}

Sprawdzanie wersji kompilatora

Swift obsługuje sprawdzanie wersji kompilatora: #if swift(>=5.9). Jest to przydatne dla bibliotek i frameworków, które obsługują wiele wersji Swifta. Nowe możliwości języka (na przykład makra w Swift 5.9) mogą być chronione takim sprawdzeniem.

swift
// Wsteczna zgodność
#if swift(>=5.9)
    @MainActor
    struct ModernView: View {
        var body: some View {
            Text("Nowoczesny SwiftUI")
        }
    }
#else
    struct ModernView: View {
        var body: some View {
            Text("Starszy SwiftUI")
        }
    }
#endif

Sprawdzanie dostępności modułu canImport()

Funkcja canImport(ModuleName) sprawdza, czy określony moduł jest dostępny w bieżącym środowisku budowania. Jest to najbardziej elastyczny mechanizm: nie jest powiązany z konkretną platformą. Na przykład kod używający CoreHaptics będzie kompilowany tylko na urządzeniach, gdzie ten framework jest dostępny.

swift
#if canImport(CoreHaptics)
    import CoreHaptics

    class HapticManager {
        private var engine: CHHapticEngine?

        func playTapFeedback() {
            guard let engine else { return }
            // Implementacja odpowiedzi dotykowej
        }
    }
#endif

Alternatywy w Kotlin i Android

Kotlin jako język nie ma dyrektyw preprocesora. Zamiast tego ekosystem Android oferuje trzy alternatywy: pola BuildConfig (sprawdzanie w czasie wykonywania), sourceSets (zamiana całych plików) i expect/actual (w Kotlin Multiplatform).

Source Sets w Gradle

Gradle sourceSets pozwalają na posiadanie różnych implementacji klas dla różnych wariantów lub typów budowania. W katalogu src/debug/ znajduje się implementacja dla debug, w src/release/ — dla release. Podczas budowania Gradle wybiera odpowiedni sourceSet i kompiluje tylko jego pliki.

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 w trybie release
    }
}

Expect/Actual w Kotlin Multiplatform

KMP udostępnia mechanizm expect (deklaracja w kodzie wspólnym) i actual (implementacja dla konkretnej platformy). Jest to mechanizm na etapie kompilacji: dla iOS kompilowana jest implementacja actual z iOS sourceSet, dla Android — z Android sourceSet. Niepotrzebne implementacje nie są kompilowane.

kotlin
// commonMain — deklaracja expect
expect fun getPlatformName(): String

// androidMain — actual dla Androida
actual fun getPlatformName(): String =
    "Android \${Build.VERSION.SDK_INT}"

// iosMain — actual dla iOS
actual fun getPlatformName(): String =
    UIDevice.current.systemName() + " " + UIDevice.current.systemVersion

Preprocesor C/C++ i NDK

Podczas tworzenia natywnych bibliotek przez Android NDK używany jest klasyczny preprocesor C/C++ z dyrektywami #ifdef, #ifndef, #define. W przeciwieństwie do Swifta, preprocesor C działa na poziomie tekstowym — kod w nieaktywnych gałęziach może być składniowo niepoprawny.

Flagi platformowe NDK

NDK definiuje makra dla każdej platformy: __ANDROID__ (Android), __APPLE__ (iOS/macOS), __linux__ (Linux). Dla architektur: __arm__, __aarch64__, __x86_64__. Te makra są ustawiane automatycznie przez kompilator podczas budowania dla platformy docelowej.

cpp
// Kod natywny dla Androida i 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

Podczas pracy z NDK ważne jest, aby pamiętać, że preprocesor C/C++ to zamiana tekstowa. Jeśli w nieaktywnej gałęzi znajduje się błąd składniowy, kompilator go nie zauważy, ale jeśli z powodu nieprawidłowego #define zepsuje się aktywna gałąź — błąd się ujawni. Zaleca się minimalizowanie łańcuchów #define i używanie stałych constexpr.

Dla Rust, który również jest używany w tworzeniu aplikacji mobilnych przez UniFFI i Mozilla Application Services, istnieje własny mechanizm — feature flags w Cargo.toml. Flagi typu #[cfg(target_os = "android")] w Rust działają podobnie jak dyrektywy Swifta: sprawdzanie odbywa się na poziomie kompilatora, a nie preprocesora. To czyni Rust atrakcyjnym wyborem dla natywnych bibliotek, które muszą być kompilowane pod Android i iOS z jednej bazy kodu.

Praktyczne scenariusze i antywzorce

Kompilacja warunkowa jest skuteczna w ściśle określonych scenariuszach. Przy nieprawidłowym użyciu tworzy kod o nieprzyjemnym zapachu, który jest trudny do testowania i utrzymania. Rozważmy poprawne scenariusze i typowe błędy.

Poprawne scenariusze

Pierwszy scenariusz — abstrakcja platformowa: jednolity fasad, wewnątrz którego kompilacja warunkowa wybiera implementację platformową. Drugi — debugowanie i profilowanie: narzędzia programistyczne, które nie powinny trafiać do wydania. Trzeci — wsteczna zgodność: obsługa starych wersji systemu, dopóki nie zostanie zaktualizowana minimalna wersja.

ScenariuszJęzykWarunek
Abstrakcja platformowaSwift#if os(iOS)
DebugowanieSwift/ObjC#if DEBUG
Wsteczna zgodnośćSwift#if swift(>=5.7)
Biblioteka natywnaC/C++#ifdef __ANDROID__
Testowanie A/BJava/KotlinBuildConfig.FLAVOR

Antywzorce

Najbardziej niebezpieczny antywzorzec — rozprzestrzenianie dyrektyw po całym kodzie. Jeśli co drugi plik zawiera #if, to sygnał, że architektura wymaga refaktoryzacji. Prawidłowym rozwiązaniem jest wydzielenie kodu platformowego za protokoły/interfejsy i użycie Dependency Injection.

  • #if w każdym pliku — antywzorzec architektoniczny. Kod platformowy powinien być izolowany za protokołami.
  • Zagnieżdżone #if — szybko stają się nieczytelne. Głębokość zagnieżdżenia nie powinna przekraczać 2 poziomów.
  • Powielanie całych funkcji — jeśli funkcja jest w całości skopiowana w #if i #else, należy ją wydzielić do wspólnej części.
  • Testowanie — kod wewnątrz nieaktywnych gałęzi nie jest testowany. Niezbędne są CI z budowaniem wszystkich możliwych kombinacji.
  • Magiczne flagi — nieudokumentowane flagi, o których nie wie nowy zespół programistów.

Często zadawane pytania

Czym różni się kompilacja warunkowa od sprawdzania w czasie wykonywania?

Kompilacja warunkowa działa na etapie kompilacji: nieaktywny kod nie trafia do pliku binarnego. Sprawdzanie w czasie wykonywania (if / switch) jest kompilowane zawsze, warunek sprawdzany jest podczas wykonania. Pierwsze jest bezpieczniejsze i wydajniejsze, drugie — bardziej elastyczne (można zmienić bez przebudowywania).

Czy można używać #if wewnątrz funkcji w Swift?

Tak, Swift dopuszcza dyrektywy #if wewnątrz funkcji, pętli, a nawet wewnątrz wyrażeń. To jedna z możliwości, której brakowało we wczesnych wersjach Swifta. Na przykład: let x = #if DEBUG 1 #else 0 #endif — poprawny kod.

Dlaczego Kotlin nie dodał preprocesora?

Twórcy Kotlina świadomie zrezygnowali z preprocesora, uważając go za źródło kruchego kodu. Zamiast tego proponują expect/actual (bezpieczeństwo na etapie kompilacji) i Gradle sourceSets (izolacja na poziomie plików). Oba podejścia są bardziej niezawodne niż zamiana tekstowa.

Jak testować kod wewnątrz nieaktywnych gałęzi #if?

Budować aplikację z różnymi kombinacjami flag w CI. Dla Swifta: skonfigurować osobne schematy Xcode z różnymi Active Compilation Conditions. Dla Androida: skonfigurować osobne Build Variants i uruchamiać testy dla każdego. Automatyzacja jest obowiązkowa.

Co się stanie, jeśli warunek #if zawiera błąd składniowy?

W Swift warunek #if to dyrektywa kompilatora. Jeśli sam warunek jest składniowo niepoprawny (na przykład literówka w nazwie os()), kompilator zgłosi błąd kompilacji. W C/C++ preprocesor po prostu nie znajdzie makra i warunek stanie się fałszywy.

Podsumowanie

  • Kompilacja warunkowa — technika kompilacji, która wyklucza niepotrzebny kod na etapie budowania, w przeciwieństwie do sprawdzania w czasie wykonywania.
  • Swift obsługuje #if z os(), canImport(), swift() — dyrektywy bezpieczne na etapie kompilacji, wymagające poprawności składniowej wszystkich gałęzi.
  • Kotlin używa expect/actual i Gradle sourceSets zamiast preprocesora — podejścia bardziej niezawodne, ale mniej elastyczne.
  • C/C++ w NDK używa klasycznego preprocesora tekstowego #ifdef / #ifndef z makrami platformowymi __ANDROID__, __APPLE__.
  • Prawidłowe zastosowanie — abstrakcja platformowa, debugowanie, wsteczna zgodność. Nieprawidłowe — #if w każdym pliku, głębokie zagnieżdżenie, magiczne flagi.
  • CI jest obowiązkowy — wszystkie kombinacje flag muszą być budowane i testowane automatycznie, w przeciwnym razie kod w nieaktywnych gałęziach staje się martwy.

Opracujemy aplikację mobilną pod klucz

IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.

Omów projekt

Przeczytaj również