Conditional Compilation in Mobile Apps — Essence, Directives, and How It Works

Author: IT Sectr Published: 2026-06-01 Reading time: 9 min

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 — a technique for selectively compiling code based on platform, configuration, or language version conditions.
  • Directives #if, #elseif, #else, #endif — the main conditional compilation constructs in Swift, C, C++, Objective-C.
  • Kotlin has no preprocessor directives — instead, BuildConfig, expect/actual, and sourceSets are used.
  • Advantage — code for unsuitable platforms is not compiled, reducing binary size and eliminating errors.
  • iOS/macOS shared code — Conditional Compilation is the foundation of cross-platform Apple framework development.

What is Conditional Compilation

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.

Conditional Compilation in Swift

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.

Platform Checks os()

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.

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

Compiler Version Checks

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.

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

Module Availability Check canImport()

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.

swift
#if canImport(CoreHaptics)
    import CoreHaptics

    class HapticManager {
        private var engine: CHHapticEngine?

        func playTapFeedback() {
            guard let engine else { return }
            // Haptic feedback implementation
        }
    }
#endif

Alternatives in Kotlin and Android

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

Source Sets in Gradle

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.

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

Expect/Actual in Kotlin Multiplatform

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.

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

C/C++ Preprocessor and NDK

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 Platform Flags

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.

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

Practical Scenarios and Antipatterns

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.

Correct Scenarios

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.

ScenarioLanguageCondition
Platform AbstractionSwift#if os(iOS)
DebuggingSwift/ObjC#if DEBUG
Backward CompatibilitySwift#if swift(>=5.7)
Native LibraryC/C++#ifdef __ANDROID__
A/B TestingJava/KotlinBuildConfig.FLAVOR

Antipatterns

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.

  • #if in every file — an architectural antipattern. Platform code should be isolated behind protocols.
  • Nested #if — quickly becomes unreadable. Nesting depth should not exceed 2 levels.
  • Duplicating entire functions — if a function is completely copied in #if and #else, it should be extracted to a common part.
  • Testing — code inside inactive branches is not tested. CI builds of all possible combinations are necessary.
  • Magic flags — undocumented flags that the new development team doesn't know about.

Frequently Asked Questions

How is Conditional Compilation different from runtime checks?

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

Can #if be used inside a function in Swift?

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.

Why didn't Kotlin add a preprocessor?

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.

How to test code inside inactive #if branches?

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.

What happens if the #if condition has a syntax error?

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

  • Conditional Compilation — a compilation technique that excludes non-target code at build time, unlike runtime checks.
  • Swift supports #if with os(), canImport(), swift() — compile-time safe directives requiring syntactic correctness of all branches.
  • Kotlin uses expect/actual and Gradle sourceSets instead of a preprocessor — more reliable but less flexible approaches.
  • C/C++ in NDK uses the classic text preprocessor #ifdef / #ifndef with platform macros __ANDROID__, __APPLE__.
  • Proper use — platform abstraction, debugging, backward compatibility. Improper use — #if in every file, deep nesting, magic flags.
  • CI is mandatory — all flag combinations must be built and tested automatically, otherwise code in inactive branches becomes dead.

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.

Discuss the project

Read also