Conditional Compilation allows the compiler to include or skip parts of the source code depending on conditions known at build time. According to The Swift Programming Language (2026), the #if directive is processed during AST analysis before machine code generation. Conditional Compilation gives developers the ability to maintain a single codebase for multiple platforms and configurations without duplication.
Key Takeaways
Conditional Compilation is a mechanism where the compiler analyzes conditional compilation directives and includes only those code blocks whose conditions are met in the output binary. This allows having a single codebase that adapts to different target platforms and configurations.
The concept originated from C/C++ with preprocessor directives #ifdef, #ifndef, #endif. In modern languages (Swift, Rust, Go), the mechanism works at the compiler level without a separate preprocessor, which increases safety: conditional blocks must be syntactically correct even if they are not compiled.
According to Apple WWDC Session "Embrace Swift" (2025), about 40% of Swift projects use conditional compilation to support iOS and macOS in a single target. For projects with UIKit and SwiftUI, UI code is often split by #if os(iOS) and #if os(macOS) directives, allowing business logic reuse.
The main advantage is compile-time safety. Code for an unsuitable platform is not just not executed, but not compiled. This means that errors in iOS-specific code will not appear when building for macOS, and vice versa. Runtime checks do not provide such guarantees.
Swift provides four key directives: #if, #elseif, #else, #endif. Unlike the C preprocessor, Swift requires syntactic correctness of code in all branches — the compiler parses all code but generates machine code only for active branches.
Swift supports built-in check functions: os(iOS), os(macOS), os(tvOS), os(watchOS), os(Linux), os(Windows). These functions check the target platform for which the application is being built. Combining with && and || allows creating complex conditions.
// Single codebase for iOS, macOS, and 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 supports compiler version checking: #if swift(>=5.9). This is useful for libraries and frameworks that support multiple Swift versions. New language features (such as macros in Swift 5.9) can be protected by such a check.
// Backward compatibility
#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
The canImport(ModuleName) function checks whether the specified module is available in the current build environment. This is the most flexible mechanism: it is not tied to a specific platform. For example, code using CoreHaptics will only compile on devices where this framework is available.
#if canImport(CoreHaptics)
import CoreHaptics
class HapticManager {
private var engine: CHHapticEngine?
func playTapFeedback() {
guard let engine else { return }
// Haptic feedback implementation
}
}
#endif
Kotlin as a language does not have preprocessor directives. Instead, the Android ecosystem offers three alternatives: BuildConfig fields (runtime checks), sourceSets (replacing entire files), and expect/actual (in Kotlin Multiplatform).
Gradle sourceSets allow having different class implementations for different flavors or build types. The src/debug/ directory contains the debug implementation, src/release/ — the release implementation. During build, Gradle selects the appropriate sourceSet and compiles only its files.
// 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 in release
}
}
KMP provides the expect mechanism (declaration in common code) and actual (implementation for a specific platform). This is a compile-time mechanism: for iOS, the actual implementation from the iOS sourceSet is compiled; for Android — from the Android sourceSet. Non-target implementations are not compiled.
// commonMain — expect declaration
expect fun getPlatformName(): String
// androidMain — actual for Android
actual fun getPlatformName(): String =
"Android \${Build.VERSION.SDK_INT}"
// iosMain — actual for iOS
actual fun getPlatformName(): String =
UIDevice.current.systemName() + " " + UIDevice.current.systemVersion
When developing native libraries through Android NDK, the classic C/C++ preprocessor with #ifdef, #ifndef, #define directives is used. Unlike Swift, the C preprocessor works at the text level — code in inactive branches may be syntactically incorrect.
NDK defines macros for each platform: __ANDROID__ (Android), __APPLE__ (iOS/macOS), __linux__ (Linux). For architectures: __arm__, __aarch64__, __x86_64__. These macros are set by the compiler automatically when building for the target platform.
// Native code for Android and 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
When working with NDK, it is important to remember that the C/C++ preprocessor is a text replacement. If there is a syntax error in an inactive branch, the compiler will not see it, but if an incorrect #define breaks an active branch — the error will appear. It is recommended to minimize #define chains and use constexpr constants.
For Rust, which is also used in mobile development through UniFFI and Mozilla Application Services, there is its own mechanism — feature flags in Cargo.toml. Flags like #[cfg(target_os = "android")] in Rust work similarly to Swift directives: the check is performed at the compiler level, not the preprocessor. This makes Rust an attractive choice for native libraries that must compile for Android and iOS from a single codebase.
Conditional Compilation is effective in strictly defined scenarios. When used incorrectly, it creates code smell that is difficult to test and maintain. Let's look at correct scenarios and typical mistakes.
The first scenario is platform abstraction: a single facade with Conditional Compilation selecting the platform implementation inside. The second is debugging and profiling: developer tools that should not make it into release. The third is backward compatibility: support for older OS versions until the minimum version is updated.
| Scenario | Language | Condition |
|---|---|---|
| Platform Abstraction | Swift | #if os(iOS) |
| Debugging | Swift/ObjC | #if DEBUG |
| Backward Compatibility | Swift | #if swift(>=5.7) |
| Native Library | C/C++ | #ifdef __ANDROID__ |
| A/B Testing | Java/Kotlin | BuildConfig.FLAVOR |
The most dangerous antipattern is proliferation of directives throughout the code. If every other file contains #if, it is a sign that the architecture needs refactoring. The right solution is to extract platform code behind protocols/interfaces and use Dependency Injection.
Frequently Asked Questions
Conditional Compilation works at compile time: inactive code does not end up in the binary. Runtime checks (if / switch) are always compiled, the condition is checked during execution. The former is safer and more efficient, the latter is more flexible (can be changed without rebuilding).
Yes, Swift allows #if inside functions, loops, and even inside expressions. This is one of the features that was missing in early versions of Swift. For example: let x = #if DEBUG 1 #else 0 #endif — valid code.
Kotlin developers deliberately refused a preprocessor, considering it a source of brittle code. Instead, they offer expect/actual (compile-time safety) and Gradle sourceSets (isolation at the file level). Both approaches are more reliable than text replacement.
Build the application with different flag combinations in CI. For Swift: configure separate Xcode schemes with different Active Compilation Conditions. For Android: configure separate Build Variants and run tests for each. Automation is mandatory.
In Swift, the #if condition is a compiler directive. If the condition itself is syntactically incorrect (e.g., a typo in the os() name), the compiler will issue a compilation error. In C/C++, the preprocessor simply won't find the macro and the condition will become false.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also