条件付きコンパイル(Conditional Compilation)を使用すると、ビルド時に既知の条件に基づいて、コンパイラがソースコードの一部を含めたりスキップしたりできます。The Swift Programming Language (2026)によると、#ifディレクティブはマシンコード生成前のAST解析中に処理されます。条件付きコンパイルにより、開発者は複数のプラットフォームや構成に対して、重複なしで単一のコードベースを維持できます。
重要なポイント
条件付きコンパイルは、コンパイラが条件付きコンパイルディレクティブを解析し、条件を満たすコードブロックのみを出力バイナリに含めるメカニズムです。これにより、異なるターゲットプラットフォームや構成に適応する単一のコードベースを維持できます。
この概念はC/C++のプリプロセッサディレクティブ#ifdef、#ifndef、#endifに由来します。現代の言語(Swift、Rust、Go)では、このメカニズムは個別のプリプロセッサなしでコンパイラレベルで動作し、安全性が向上します。条件付きブロックは、コンパイルされない場合でも構文的に正しい必要があります。
Apple WWDCセッション「Embrace Swift」(2025)によると、約40%のSwiftプロジェクトが単一ターゲットでiOSとmacOSをサポートするために条件付きコンパイルを使用しています。UIKitとSwiftUIを使用するプロジェクトでは、UIコードは#if os(iOS)および#if os(macOS)ディレクティブで分割されることが多く、ビジネスロジックの再利用が可能になります。
主な利点はコンパイル時の安全性です。不適切なプラットフォームのコードは実行されないだけでなく、コンパイルもされません。つまり、iOS固有のコードのエラーはmacOS用にビルドするときに現れず、その逆も同様です。ランタイムチェックではそのような保証はできません。
Swiftは4つの主要ディレクティブを提供します:#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エコシステムは3つの代替手段を提供します:BuildConfigフィールド(ランタイムチェック)、sourceSets(ファイル全体の置き換え)、およびexpect/actual(Kotlin Multiplatform)。
GradleのsourceSetsを使用すると、異なるフレーバーやビルドタイプに対して異なるクラス実装を持つことができます。src/debug/ディレクトリにはデバッグ実装が、src/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
}
}
KMPはexpectメカニズム(共通コードでの宣言)とactual(特定のプラットフォームでの実装)を提供します。これはコンパイル時のメカニズムです:iOSの場合はiOS sourceSetからactual実装がコンパイルされ、Androidの場合はAndroid sourceSetからコンパイルされます。対象外の実装はコンパイルされません。
// commonMain — expect宣言
expect fun getPlatformName(): String
// androidMain — Android用actual
actual fun getPlatformName(): String =
"Android \${Build.VERSION.SDK_INT}"
// iosMain — iOS用actual
actual fun getPlatformName(): String =
UIDevice.current.systemName() + " " + UIDevice.current.systemVersion
Android NDKを通じてネイティブライブラリを開発する場合、#ifdef、#ifndef、#defineディレクティブを持つ従来のC/C++プリプロセッサが使用されます。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定数を使用することをお勧めします。
UniFFIやMozilla Application Servicesを通じてモバイル開発でも使用されるRustには、独自のメカニズムがあります — Cargo.tomlのフィーチャーフラグです。#[cfg(target_os = “android”)]のようなRustのフラグは、Swiftディレクティブと同様に機能します:チェックはプリプロセッサではなくコンパイラレベルで実行されます。これにより、Rustは単一のコードベースからAndroidとiOS用にコンパイルする必要があるネイティブライブラリにとって魅力的な選択肢となっています。
条件付きコンパイルは厳密に定義されたシナリオで効果的です。誤って使用すると、テストと保守が困難なコードスメルを生み出します。正しいシナリオと典型的な間違いを見てみましょう。
最初のシナリオはプラットフォーム抽象化です:内部で条件付きコンパイルがプラットフォーム実装を選択する単一のファサード。2つ目はデバッグとプロファイリング:リリースに含めるべきでない開発者ツール。3つ目は後方互換性:最小バージョンが更新されるまでの古いOSバージョンのサポート。
| シナリオ | 言語 | 条件 |
|---|---|---|
| プラットフォーム抽象化 | Swift | #if os(iOS) |
| デバッグ | Swift/ObjC | #if DEBUG |
| 後方互換性 | Swift | #if swift(>=5.7) |
| ネイティブライブラリ | C/C++ | #ifdef __ANDROID__ |
| A/Bテスト | Java/Kotlin | BuildConfig.FLAVOR |
最も危険なアンチパターンはコード全体へのディレクティブの蔓延です。2つおきのファイルに#ifが含まれている場合、それはアーキテクチャのリファクタリングが必要な兆候です。正しい解決策は、プラットフォームコードをプロトコル/インターフェースの背後に抽出し、依存性注入を使用することです。
よくある質問
条件付きコンパイルはコンパイル時に機能します:非アクティブなコードはバイナリに含まれません。ランタイムチェック(if / switch)は常にコンパイルされ、条件は実行時にチェックされます。前者はより安全で効率的であり、後者はより柔軟です(再ビルドなしで変更可能)。
はい、Swiftは関数内での#if、ループ内、さらには式内でもディレクティブを許可します。これはSwiftの初期バージョンにはなかった機能の1つです。例:let x = #if DEBUG 1 #else 0 #endif — 有効なコードです。
Kotlinの開発者は、プリプロセッサを脆弱なコードの原因と見なし、意図的に採用しませんでした。代わりに、expect/actual(コンパイル時の安全性)とGradleのsourceSets(ファイルレベルの分離)を提供しています。どちらのアプローチもテキスト置換よりも信頼性があります。
CIで異なるフラグの組み合わせでアプリケーションをビルドします。Swiftの場合:異なるActive Compilation Conditionsを持つ個別のXcodeスキームを構成します。Androidの場合:個別のBuild Variantsを構成し、それぞれに対してテストを実行します。自動化は必須です。
Swiftでは、#if条件はコンパイラディレクティブです。条件自体が構文的に正しくない場合(例:os()名のタイプミス)、コンパイラはコンパイルエラーを発行します。C/C++では、プリプロセッサが単にマクロを見つけられず、条件は偽になります。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。