Conditional Compilation дозволяє компілятору включати або пропускати частини вихідного коду залежно від умов, відомих на етапі збірки. Згідно з The Swift Programming Language (2026), директива #if обробляється на етапі AST-аналізу до генерації машинного коду. 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-перевірки такої гарантії не дають.
Swift надає чотири ключові директиви: #if, #elseif, #else, #endif. На відміну від C препроцесора, Swift вимагає синтаксичної правильності коду в усіх гілках — компілятор парсить весь код, але генерує машинний код лише для активних гілок.
Swift підтримує вбудовані функції перевірки: os(iOS), os(macOS), os(tvOS), os(watchOS), os(Linux), os(Windows). Ці функції перевіряють цільову платформу, для якої збирається додаток. Комбінація з && та || дозволяє створювати складні умови.
// Єдиний код для 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) можуть бути захищені такою перевіркою.
// Зворотна сумісність
#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(ModuleName) перевіряє, чи доступний вказаний модуль у поточному середовищі збірки. Це найбільш гнучкий механізм: він не прив'язаний до конкретної платформи. Наприклад, код, що використовує CoreHaptics, компілюватиметься лише на пристроях, де цей фреймворк доступний.
#if canImport(CoreHaptics)
import CoreHaptics
class HapticManager {
private var engine: CHHapticEngine?
func playTapFeedback() {
guard let engine else { return }
// Реалізація тактильного відгуку
}
}
#endif
Kotlin як мова не має препроцесорних директив. Натомість Android екосистема пропонує три альтернативи: поля BuildConfig (runtime-перевірки), sourceSets (заміна цілих файлів) та expect/actual (у Kotlin Multiplatform).
Gradle sourceSets дозволяють мати різні реалізації класів для різних флейворів або типів збірки. У директорії src/debug/ лежить реалізація для debug, у src/release/ — для release. При збірці Gradle вибирає відповідний sourceSet і компілює лише його файли.
// 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
}
}
KMP надає механізм expect (оголошення в загальному коді) та actual (реалізація для конкретної платформи). Це compile-time механізм: для iOS компілюється actual-реалізація з iOS sourceSet, для Android — з Android sourceSet. Нецільові реалізації не компілюються.
// 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
При розробці нативних бібліотек через Android NDK використовується класичний препроцесор C/C++ з директивами #ifdef, #ifndef, #define. На відміну від Swift, C препроцесор працює на текстовому рівні — код у неактивних гілках може бути синтаксично некоректним.
NDK визначає макроси для кожної платформи: __ANDROID__ (Android), __APPLE__ (iOS/macOS), __linux__ (Linux). Для архітектур: __arm__, __aarch64__, __x86_64__. Ці макроси встановлюються компілятором автоматично при збірці для цільової платформи.
// Нативний код для 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/Kotlin | BuildConfig.FLAVOR |
Найнебезпечніший антипатерн — розростання директив по всьому коду. Якщо кожен другий файл містить #if, це сигнал, що архітектура потребує рефакторингу. Правильне рішення — винести платформний код за протоколи/інтерфейси та використовувати Dependency Injection.
Часті запитання
Conditional Compilation працює на етапі компіляції: неактивний код не потрапляє в бінарник. Runtime-перевірки (if / switch) компілюються завжди, умова перевіряється під час виконання. Перше — безпечніше та ефективніше, друге — гнучкіше (можна змінювати без перезбірки).
Так, Swift допускає директиви #if всередині функцій, циклів і навіть всередині виразів. Це одна з можливостей, відсутніх у ранніх версіях Swift. Наприклад: let x = #if DEBUG 1 #else 0 #endif — коректний код.
Розробники Kotlin свідомо відмовилися від препроцесора, вважаючи його джерелом крихкого коду. Натомість вони пропонують expect/actual (compile-time безпека) та Gradle sourceSets (ізоляція на рівні файлів). Обидва підходи надійніші за текстову заміну.
Збирати додаток з різними комбінаціями прапорців у CI. Для Swift: налаштувати окремі схеми Xcode з різними Active Compilation Conditions. Для Android: налаштувати окремі Build Variants та запускати тести для кожного. Автоматизація обов'язкова.
У Swift умова #if — це компіляторна директива. Якщо сама умова синтаксично некоректна (наприклад, помилка в імені os()), компілятор видасть помилку компіляції. У C/C++ препроцесор просто не знайде макрос і умова стане хибною.
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також