Conditional Compilation у мобільних додатках — суть, директиви та принцип роботи

Автор: IT Sectr Опубліковано: 2026-06-01 Час читання: 9 хв

Conditional Compilation дозволяє компілятору включати або пропускати частини вихідного коду залежно від умов, відомих на етапі збірки. Згідно з The Swift Programming Language (2026), директива #if обробляється на етапі AST-аналізу до генерації машинного коду. Conditional Compilation дає розробникам можливість підтримувати єдину кодову базу для кількох платформ і конфігурацій без дублювання.

Головне

  • Conditional Compilation — техніка вибіркової компіляції коду за умовами платформи, конфігурації або версії мови.
  • Директиви #if, #elseif, #else, #endif — основні конструкції умовної компіляції в Swift, C, C++, Objective-C.
  • Kotlin не має директив препроцесора — замість них використовуються BuildConfig, expect/actual та sourceSets.
  • Перевага — код для непідхожих платформ не компілюється, зменшуючи розмір бінарника та виключаючи помилки.
  • iOS/macOS спільний код — Conditional Compilation основа розробки крос-платформових фреймворків Apple.

Що таке Conditional Compilation

Conditional Compilation — це механізм, при якому компілятор аналізує директиви умовної компіляції та включає у вихідний бінарний файл лише ті блоки коду, умови для яких виконуються. Це дозволяє мати єдину кодову базу, яка адаптується під різні цільові платформи та конфігурації.

Концепція прийшла з C/C++ з препроцесорними директивами #ifdef, #ifndef, #endif. У сучасних мовах (Swift, Rust, Go) механізм працює на рівні компілятора без окремого препроцесора, що підвищує безпеку: умовні блоки повинні бути синтаксично коректними, навіть якщо вони не компілюються.

За даними Apple WWDC Session «Embrace Swift» (2025), близько 40% Swift-проектів використовують умовну компіляцію для підтримки iOS та macOS в одному таргеті. Для проектів з UIKit та SwiftUI код UI часто розділений директивами #if os(iOS) та #if os(macOS), що дозволяє перевикористовувати бізнес-логіку.

Головна перевага — compile-time safety. Код для непідходящої платформи не просто не виконується, а не компілюється. Це означає, що помилки в iOS-специфічному коді не проявляться при збірці для macOS, і навпаки. Runtime-перевірки такої гарантії не дають.

Conditional Compilation у Swift

Swift надає чотири ключові директиви: #if, #elseif, #else, #endif. На відміну від C препроцесора, Swift вимагає синтаксичної правильності коду в усіх гілках — компілятор парсить весь код, але генерує машинний код лише для активних гілок.

Платформенні перевірки os()

Swift підтримує вбудовані функції перевірки: os(iOS), os(macOS), os(tvOS), os(watchOS), os(Linux), os(Windows). Ці функції перевіряють цільову платформу, для якої збирається додаток. Комбінація з && та || дозволяє створювати складні умови.

swift
// Єдиний код для iOS, macOS та 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 підтримує перевірку версії компілятора: #if swift(>=5.9). Це корисно для бібліотек та фреймворків, які підтримують кілька версій Swift. Нові можливості мови (наприклад, макроси у Swift 5.9) можуть бути захищені такою перевіркою.

swift
// Зворотна сумісність
#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

Перевірка доступності модуля canImport()

Функція canImport(ModuleName) перевіряє, чи доступний вказаний модуль у поточному середовищі збірки. Це найбільш гнучкий механізм: він не прив'язаний до конкретної платформи. Наприклад, код, що використовує CoreHaptics, компілюватиметься лише на пристроях, де цей фреймворк доступний.

swift
#if canImport(CoreHaptics)
    import CoreHaptics

    class HapticManager {
        private var engine: CHHapticEngine?

        func playTapFeedback() {
            guard let engine else { return }
            // Реалізація тактильного відгуку
        }
    }
#endif

Альтернативи в Kotlin та Android

Kotlin як мова не має препроцесорних директив. Натомість Android екосистема пропонує три альтернативи: поля BuildConfig (runtime-перевірки), sourceSets (заміна цілих файлів) та expect/actual (у Kotlin Multiplatform).

Source Sets у Gradle

Gradle sourceSets дозволяють мати різні реалізації класів для різних флейворів або типів збірки. У директорії src/debug/ лежить реалізація для debug, у src/release/ — для release. При збірці Gradle вибирає відповідний sourceSet і компілює лише його файли.

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 у release
    }
}

Expect/Actual у Kotlin Multiplatform

KMP надає механізм expect (оголошення в загальному коді) та actual (реалізація для конкретної платформи). Це compile-time механізм: для iOS компілюється actual-реалізація з iOS sourceSet, для Android — з Android sourceSet. Нецільові реалізації не компілюються.

kotlin
// commonMain — expect оголошення
expect fun getPlatformName(): String

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

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

Препроцесор C/C++ та NDK

При розробці нативних бібліотек через Android NDK використовується класичний препроцесор C/C++ з директивами #ifdef, #ifndef, #define. На відміну від Swift, C препроцесор працює на текстовому рівні — код у неактивних гілках може бути синтаксично некоректним.

Платформенні прапорці NDK

NDK визначає макроси для кожної платформи: __ANDROID__ (Android), __APPLE__ (iOS/macOS), __linux__ (Linux). Для архітектур: __arm__, __aarch64__, __x86_64__. Ці макроси встановлюються компілятором автоматично при збірці для цільової платформи.

cpp
// Нативний код для Android та 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

При роботі з NDK важливо пам'ятати, що препроцесор C/C++ — це текстова заміна. Якщо в неактивній гілці є синтаксична помилка, компілятор її не побачить, але якщо через неправильний #define зламається активна гілка — помилка проявиться. Рекомендується мінімізувати ланцюжки #define та використовувати константи constexpr.

Для Rust, який також використовується в мобільній розробці через UniFFI та Mozilla Application Services, існує власний механізм — feature flags у Cargo.toml. Прапорці виду #[cfg(target_os = «android»)] у Rust працюють аналогічно Swift директивам: перевірка виконується на рівні компілятора, а не препроцесора. Це робить Rust привабливим вибором для нативних бібліотек, які повинні компілюватися під Android та iOS з однієї кодової бази.

Практичні сценарії та антипатерни

Conditional Compilation ефективна в строго визначених сценаріях. При неправильному використанні вона створює код із запахом (code smell), який складно тестувати та підтримувати. Розглянемо коректні сценарії та типові помилки.

Коректні сценарії

Перший сценарій — платформова абстракція: єдиний фасад, всередині якого Conditional Compilation вибирає платформну реалізацію. Другий — налагодження та профілювання: інструменти розробника, які не повинні потрапляти в реліз. Третій — зворотна сумісність: підтримка старих версій ОС, поки не оновиться мінімальна версія.

СценарійМоваУмова
Платформова абстракціяSwift#if os(iOS)
НалагодженняSwift/ObjC#if DEBUG
Зворотна сумісністьSwift#if swift(>=5.7)
Нативна бібліотекаC/C++#ifdef __ANDROID__
A/B тестуванняJava/KotlinBuildConfig.FLAVOR

Антипатерни

Найнебезпечніший антипатерн — розростання директив по всьому коду. Якщо кожен другий файл містить #if, це сигнал, що архітектура потребує рефакторингу. Правильне рішення — винести платформний код за протоколи/інтерфейси та використовувати Dependency Injection.

  • #if у кожному файлі — архітектурний антипатерн. Платформний код має бути ізольований за протоколами.
  • Вкладені #if — швидко стають нечитабельними. Глибина вкладеності не повинна перевищувати 2 рівні.
  • Дублювання цілих функцій — якщо функція повністю скопійована в #if та #else, її потрібно винести в загальну частину.
  • Тестування — код усередині неактивних гілок не тестується. Необхідні CI збірки всіх можливих комбінацій.
  • Магічні прапорці — undocumented прапорці, про які не знає нова команда розробників.

Часті запитання

Чим Conditional Compilation відрізняється від runtime-перевірок?

Conditional Compilation працює на етапі компіляції: неактивний код не потрапляє в бінарник. Runtime-перевірки (if / switch) компілюються завжди, умова перевіряється під час виконання. Перше — безпечніше та ефективніше, друге — гнучкіше (можна змінювати без перезбірки).

Чи можна використовувати #if всередині функції в Swift?

Так, Swift допускає директиви #if всередині функцій, циклів і навіть всередині виразів. Це одна з можливостей, відсутніх у ранніх версіях Swift. Наприклад: let x = #if DEBUG 1 #else 0 #endif — коректний код.

Чому Kotlin не додав препроцесор?

Розробники Kotlin свідомо відмовилися від препроцесора, вважаючи його джерелом крихкого коду. Натомість вони пропонують expect/actual (compile-time безпека) та Gradle sourceSets (ізоляція на рівні файлів). Обидва підходи надійніші за текстову заміну.

Як тестувати код всередині неактивних гілок #if?

Збирати додаток з різними комбінаціями прапорців у CI. Для Swift: налаштувати окремі схеми Xcode з різними Active Compilation Conditions. Для Android: налаштувати окремі Build Variants та запускати тести для кожного. Автоматизація обов'язкова.

Що буде, якщо умова #if містить синтаксичну помилку?

У Swift умова #if — це компіляторна директива. Якщо сама умова синтаксично некоректна (наприклад, помилка в імені os()), компілятор видасть помилку компіляції. У C/C++ препроцесор просто не знайде макрос і умова стане хибною.

Підсумки

  • Conditional Compilation — техніка компіляції, що виключає нецільовий код на етапі збірки, на відміну від runtime-перевірок.
  • Swift підтримує #if з os(), canImport(), swift() — compile-time безпечними директивами, що вимагають синтаксичної коректності всіх гілок.
  • Kotlin використовує expect/actual та Gradle sourceSets замість препроцесора — більш надійні, але менш гнучкі підходи.
  • C/C++ у NDK використовує класичний текстовий препроцесор #ifdef / #ifndef з платформовими макросами __ANDROID__, __APPLE__.
  • Правильне застосування — платформова абстракція, налагодження, зворотна сумісність. Неправильне — #if у кожному файлі, глибока вкладеність, магічні прапорці.
  • CI обов'язковий — всі комбінації прапорців повинні збиратися та тестуватися автоматично, інакше код у неактивних гілках стає мертвим.

Ми розробимо мобільний застосунок під ключ

IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.

Обговорити проект

Читайте також