Swift Language
Programming
Code Replacement
ifdef Replacement
Software Development

ifdef replacement in the Swift language

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Swift does not use C-style #ifdef and #ifndef macros for most conditional compilation tasks. The replacement is Swift's own #if system with built-in conditions such as os, arch, canImport, and custom compilation flags defined in build settings. For API availability at runtime, Swift uses if #available, which solves a different problem from compile-time exclusion.

Use Swift Conditional Compilation, Not the C Preprocessor

Swift supports compile-time conditions with directives such as:

  • '#if'
  • '#elseif'
  • '#else'
  • '#endif'

A basic example:

swift
1#if DEBUG
2print("Debug build")
3#else
4print("Release build")
5#endif

This is the closest direct replacement for the old #ifdef DEBUG style.

The key difference is that Swift does not encourage arbitrary macro programming the way C does. The language offers a smaller, more structured set of compile-time conditions.

Built-In Conditions You Will Actually Use

Swift provides several useful conditions out of the box.

Operating system:

swift
1#if os(iOS)
2print("iOS")
3#elseif os(macOS)
4print("macOS")
5#endif

Architecture:

swift
#if arch(arm64)
print("Running on arm64 build")
#endif

Simulator versus device:

swift
1#if targetEnvironment(simulator)
2print("Simulator build")
3#else
4print("Real device build")
5#endif

Module presence:

swift
#if canImport(UIKit)
import UIKit
#endif

These cover most real-world cases where developers coming from C or Objective-C would previously reach for #ifdef.

Define Your Own Flags

Custom flags in Swift are usually configured in the build settings rather than written in source headers.

In Xcode, add a value under Active Compilation Conditions, for example INTERNAL_BUILD. Then use it like this:

swift
#if INTERNAL_BUILD
print("Internal diagnostics enabled")
#endif

If you compile from the command line, you can pass a flag with -D.

bash
swiftc -D INTERNAL_BUILD main.swift -o main

This is the proper Swift replacement for many old #define FEATURE_X plus #ifdef FEATURE_X workflows.

if #available Solves a Different Problem

A common point of confusion is mixing compile-time conditions with runtime API availability.

If you need to check whether a device can call a newer API at runtime, use if #available, not #if.

swift
1import Foundation
2
3if #available(iOS 17, macOS 14, *) {
4    print("Use the newer API path")
5} else {
6    print("Fallback for older systems")
7}

Why the distinction matters:

  • '#if removes code at compile time based on build conditions'
  • 'if #available keeps both branches in the binary and chooses at runtime based on OS version'

These are not interchangeable.

A Practical Example

Suppose you want debug-only logging on iOS, but only when building an internal version.

swift
1#if os(iOS) && INTERNAL_BUILD
2func logDebug(_ message: String) {
3    print("[DEBUG] \(message)")
4}
5#else
6func logDebug(_ message: String) {
7}
8#endif

That is expressive enough for most build-configuration cases without falling back to a macro-heavy style.

Prefer Normal Swift Design When Possible

Not every variation should become a compile-time flag. Sometimes a protocol, configuration object, or dependency injection is the better design.

Use compile-time conditions when the code genuinely differs by:

  • platform
  • build type
  • available module
  • target environment

Use ordinary runtime code when behavior can vary without changing what gets compiled.

Overusing compilation flags can make the codebase harder to test and reason about.

Common Pitfalls

A common mistake is trying to recreate the C preprocessor mentally and expecting arbitrary macro substitution. Swift does not work that way.

Another mistake is using #if when the real question is OS availability at runtime. That should be if #available.

People also forget that custom flags must be defined in build settings or passed with -D. Writing #if MYFLAG in source does nothing unless the flag exists.

Finally, canImport is often better than platform checks when the real dependency is the presence of a framework rather than the platform name itself.

Summary

  • Swift replaces most #ifdef use cases with #if, #elseif, #else, and #endif
  • Built-in conditions such as os, arch, targetEnvironment, and canImport cover common compile-time decisions
  • Custom flags are added through Active Compilation Conditions in Xcode or -D on the command line
  • Use if #available for runtime API availability checks, not as a replacement for compile-time flags
  • Prefer normal Swift design over conditional compilation when the behavior does not truly depend on build context
  • Think in terms of compile-time conditions and runtime checks as separate tools with different purposes

Course illustration
Course illustration