モバイルアプリにおける条件付きコンパイル — 本質、ディレクティブ、動作原理

著者: IT Sectr 公開日: 2026-06-01 読了時間: 9 分

条件付きコンパイル(Conditional Compilation)を使用すると、ビルド時に既知の条件に基づいて、コンパイラがソースコードの一部を含めたりスキップしたりできます。The Swift Programming Language (2026)によると、#ifディレクティブはマシンコード生成前のAST解析中に処理されます。条件付きコンパイルにより、開発者は複数のプラットフォームや構成に対して、重複なしで単一のコードベースを維持できます。

重要なポイント

  • 条件付きコンパイル — プラットフォーム、構成、言語バージョンの条件に基づいてコードを選択的にコンパイルする手法。
  • ディレクティブ #if、#elseif、#else、#endif — Swift、C、C++、Objective-Cにおける主要な条件付きコンパイル構文。
  • Kotlinにはプリプロセッサディレクティブがありません — 代わりにBuildConfig、expect/actual、sourceSetsが使用されます。
  • 利点 — 不適切なプラットフォームのコードがコンパイルされないため、バイナリサイズが削減され、エラーが排除されます。
  • iOS/macOS共有コード — 条件付きコンパイルはAppleのクロスプラットフォームフレームワーク開発の基盤です。

条件付きコンパイルとは

条件付きコンパイルは、コンパイラが条件付きコンパイルディレクティブを解析し、条件を満たすコードブロックのみを出力バイナリに含めるメカニズムです。これにより、異なるターゲットプラットフォームや構成に適応する単一のコードベースを維持できます。

この概念は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での条件付きコンパイル

Swiftは4つの主要ディレクティブを提供します:#if#elseif#else#endif。Cプリプロセッサとは異なり、Swiftはすべてのブランチでコードの構文上の正確性を要求します — コンパイラはすべてのコードを解析しますが、アクティブなブランチに対してのみマシンコードを生成します。

プラットフォームチェック os()

Swiftは組み込みのチェック関数をサポートしています:os(iOS)os(macOS)os(tvOS)os(watchOS)os(Linux)os(Windows)。これらの関数は、アプリケーションがビルドされているターゲットプラットフォームを確認します。&&||との組み合わせにより、複雑な条件を作成できます。

swift
// 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のマクロなど)は、このようなチェックで保護できます。

swift
// 後方互換性
#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()

canImport(ModuleName)関数は、指定されたモジュールが現在のビルド環境で利用可能かどうかを確認します。これは最も柔軟なメカニズムです:特定のプラットフォームに縛られません。例えば、CoreHapticsを使用するコードは、このフレームワークが利用可能なデバイスでのみコンパイルされます。

swift
#if canImport(CoreHaptics)
    import CoreHaptics

    class HapticManager {
        private var engine: CHHapticEngine?

        func playTapFeedback() {
            guard let engine else { return }
            // 触覚フィードバックの実装
        }
    }
#endif

KotlinとAndroidでの代替手段

Kotlin言語にはプリプロセッサディレクティブがありません。代わりに、Androidエコシステムは3つの代替手段を提供します:BuildConfigフィールド(ランタイムチェック)、sourceSets(ファイル全体の置き換え)、およびexpect/actual(Kotlin Multiplatform)。

GradleのSource Sets

GradleのsourceSetsを使用すると、異なるフレーバーやビルドタイプに対して異なるクラス実装を持つことができます。src/debug/ディレクトリにはデバッグ実装が、src/release/にはリリース実装があります。ビルド時に、Gradleは適切なsourceSetを選択し、そのファイルのみをコンパイルします。

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

Kotlin MultiplatformのExpect/Actual

KMPはexpectメカニズム(共通コードでの宣言)とactual(特定のプラットフォームでの実装)を提供します。これはコンパイル時のメカニズムです:iOSの場合はiOS sourceSetからactual実装がコンパイルされ、Androidの場合はAndroid sourceSetからコンパイルされます。対象外の実装はコンパイルされません。

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

C/C++プリプロセッサとNDK

Android NDKを通じてネイティブライブラリを開発する場合、#ifdef#ifndef#defineディレクティブを持つ従来のC/C++プリプロセッサが使用されます。Swiftとは異なり、Cプリプロセッサはテキストレベルで動作するため、非アクティブなブランチのコードは構文的に正しくない可能性があります。

NDKプラットフォームフラグ

NDKは各プラットフォームのマクロを定義します:__ANDROID__(Android)、__APPLE__(iOS/macOS)、__linux__(Linux)。アーキテクチャ用:__arm____aarch64____x86_64__。これらのマクロは、ターゲットプラットフォーム用にビルドするときにコンパイラによって自動的に設定されます。

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

アンチパターン

最も危険なアンチパターンはコード全体へのディレクティブの蔓延です。2つおきのファイルに#ifが含まれている場合、それはアーキテクチャのリファクタリングが必要な兆候です。正しい解決策は、プラットフォームコードをプロトコル/インターフェースの背後に抽出し、依存性注入を使用することです。

  • すべてのファイルに#if — アーキテクチャ上のアンチパターン。プラットフォームコードはプロトコルの背後に分離する必要があります。
  • ネストされた#if — すぐに読みにくくなります。ネストの深さは2レベルを超えてはいけません。
  • 関数全体の重複 — 関数が#ifと#elseで完全にコピーされている場合、共通部分に抽出する必要があります。
  • テスト — 非アクティブなブランチ内のコードはテストされません。可能なすべての組み合わせのCIビルドが必要です。
  • マジックフラグ — 新しい開発チームが知らない、文書化されていないフラグ。

よくある質問

条件付きコンパイルとランタイムチェックの違いは何ですか?

条件付きコンパイルはコンパイル時に機能します:非アクティブなコードはバイナリに含まれません。ランタイムチェック(if / switch)は常にコンパイルされ、条件は実行時にチェックされます。前者はより安全で効率的であり、後者はより柔軟です(再ビルドなしで変更可能)。

Swiftの関数内で#ifを使用できますか?

はい、Swiftは関数内での#if、ループ内、さらには式内でもディレクティブを許可します。これはSwiftの初期バージョンにはなかった機能の1つです。例:let x = #if DEBUG 1 #else 0 #endif — 有効なコードです。

Kotlinがプリプロセッサを追加しなかった理由は?

Kotlinの開発者は、プリプロセッサを脆弱なコードの原因と見なし、意図的に採用しませんでした。代わりに、expect/actual(コンパイル時の安全性)とGradleのsourceSets(ファイルレベルの分離)を提供しています。どちらのアプローチもテキスト置換よりも信頼性があります。

非アクティブな#ifブランチ内のコードをテストするには?

CIで異なるフラグの組み合わせでアプリケーションをビルドします。Swiftの場合:異なるActive Compilation Conditionsを持つ個別のXcodeスキームを構成します。Androidの場合:個別のBuild Variantsを構成し、それぞれに対してテストを実行します。自動化は必須です

#if条件に構文エラーがあるとどうなりますか?

Swiftでは、#if条件はコンパイラディレクティブです。条件自体が構文的に正しくない場合(例:os()名のタイプミス)、コンパイラはコンパイルエラーを発行します。C/C++では、プリプロセッサが単にマクロを見つけられず、条件は偽になります。

まとめ

  • 条件付きコンパイル — ランタイムチェックとは異なり、ビルド時に非ターゲットコードを除外するコンパイル手法。
  • Swiftはos()、canImport()、swift()を使用した#ifをサポート — すべてのブランチの構文上の正確性を要求する、コンパイル時に安全なディレクティブ。
  • Kotlinはプリプロセッサの代わりにexpect/actualとGradleのsourceSetsを使用 — より信頼性は高いが柔軟性に劣るアプローチ。
  • NDKのC/C++はプラットフォームマクロ__ANDROID__、__APPLE__を使用した従来のテキストプリプロセッサ#ifdef / #ifndefを使用。
  • 正しい使用法 — プラットフォーム抽象化、デバッグ、後方互換性。誤った使用法 — すべてのファイルに#if、深いネスト、マジックフラグ。
  • CIは必須 — すべてのフラグの組み合わせを自動的にビルドおよびテストする必要があります。そうしないと、非アクティブなブランチのコードがデッドコードになります。

ターンキー方式のモバイルアプリケーションを開発します

IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。

プロジェクトについて相談

こちらもお読みください