[Swift] #if os, #available, @available をつかった、コードの切り分け

     

TAGS:

#if と #available/@available をまとめました。

環境&対象

以下の環境で動作確認を行なっています。

  • macOS Monterey 12.4 beta
  • Xcode 13.3
  • iOS 15.4

背景

ケースにもよりますが、コードの一部を 特定の条件でのみ 実行したい時があります。

例えば、macOS と iOS 向けのアプリでコードを共有している場合や、異なる OS バージョン向けに1つのコードベースで対応したい時などです。

例えば、SwiftUI の .confirmationDialog という View Modifier は、iOS15/macOS12 以降で用意されたものなので、それ以前の OS 上では動作しません。このような時に 動作時の OS によって コードを切り替えたくなります。

#if : conditional compilation block

#if は、Conditional compilation block と言われるものです、

ざっくりいうと 条件によって、対象ブロックをコンパイル対象にしたり、コンパイル対象から外したりすることができるものです。(対象ブロックとはコードのことです)

conditional compilation blockの必要性

例えば、macOS と iOS 向けのアプリでコードを共有している場合に、macOS でのみ使用することができる API を呼び出す箇所は、この conditional compilation block を使って、コンパイルされるかどうかを調整しないといけません。
macOS でしか使えない API を呼び出すコードは、iOS 向けにコンパイルするとエラーになってしまうからです。

MEMO
似ているけれども異なるユースケースがあります。
最新の OS 上で動作する時と以前の OS 上で動作する時に 異なる動作を行わせたいというケースです。
こちらについては、後から出てくる #available の方で説明します。

conditional compilation block とは

Swift の言語 Reference を確認すると、以下のように定義されています。

conditional-compilation-block → if-directive-clause elseif-directive-clauses opt else-directive-clause opt endif-directive

意訳すると、conditional compilation block とは「if で始まり、elseif を複数回繰り返すかもしれず、その後に 1回 else が存在するかもしれず、最後に endif で終わる」という構造の文(statement)です。

例えば以下です。


#if os(macOS)
  let a = macOSAPI()
#elseif os(iOS)
  let a = iOSAPI()
#else
  let a = generalAPI()
#endif

Swift の if 文と似ていますが、先頭に # があることが相違点です。
if と elseif の後には、条件を記述することができるようになっています。

上記の例ではコンパイル先が macOS の時には、”let a=macOSAPI()" のみがコンパイルされ、iOS の時には、"let a = iOSAPI()" のみがコンパイルされます。それ以外の時には、"let a = generalAPI()" がコンパイルされます。該当部分のみが生成されたバイナリ内部に存在します。

conditional compilation block で指定できる条件

どのような条件を使用することができるかは、Swift の Reference にも記載されています。以下が抜粋です

os: OSを条件に設定 (設定可能値:macOS | iOS | watchOS | tvOS | Linux | Windows)
arch: アーキテクチャを条件に設定(設定可能:i386 | x86_64 | arm | arm64)
swift: Swift バージョンを条件に設定可能(設定可能値:数値 、数値.数値)
compiler: Compier の対象Swiftバージョンを条件に設定可能(設定可能値:Swift と同じ)
canImport: 対象モジュールがimport できるかを条件に設定可能(設定可能値:モジュール名)(comment参照)
targetEnvironment: 対象環境を条件に設定可能(設定可能値:simulator , macCatalyst)

comment
使用可能文字は、Swift の Reference を参照してください。

#if から始まる conditional compilation block は、コンパイル対象となるかどうかを制御します。

これは、アプリ実行時に動的に決定されるものではなく、コンパイル時に処理されるものです。

C 言語での preprocessor に似ています。(Swift では、preprocessor という単位での処理はありませんが、相当する処理になっています)

#available: availability condition

#available は、アプリ実行時に 動的に 判断させることができる availability condition と呼ばれるもので、Swift 的には、condition と呼ばれるものです。他の要素と組み合わせて、文(statement) になります。

以下は、Swift の Reference からの例です。

availability condition の必要性

先ほどみた conditional compilation block があれば、理論上は どのようなケースでも対応可能です。

ですが、問題があります。条件にマッチしなかった場合には、コードはコンパイル対象とされないので、実行モジュールには含まれません。つまり、iOS13, iOS14, ... とiOS のバージョン毎に最適なAPIをコールするようなコードを使うためには、それぞれの条件毎のバイナリ作成が必要となってしまいます。

macOS と iOS のような差異であれば バイナリを1つにする意味は無いと思いますが、iOS で 条件毎にバイナリを用意することは現実的ではありません。

ということで、アプリ動作時に 条件に応じた振る舞いを選択する仕組みが欲しくなります。

それが、availability condition です。

availability condition とは

Swift の言語 Reference を確認すると、以下のように定義されています。

availability-condition → #available ( availability-arguments )

意訳すると availability condition とは、「#available で始まりそれに続く括弧内で条件を記述する」という構造の condition です。(他の要素と組み合わせて 文(statement)になります)

例えば以下のように使用できます。


if #available(macOS 12, *) {
   macOS12_SpecialAPI()
} else {
    macOS11_GeneralAPI()
}

見ての通り、Swift の if 文の条件節に使うことが可能です。
if だけに限りません。condition ですので、guard や while 等へも使用することができます。

availability condition で指定できる条件

#available に続く 括弧の中身は、availability-arguments と呼ばれているもので、プラットフォーム名 プラットフォームバージョン という形で構成されます。

プラットフォーム名への設定可能値は、iOS, iOSApplicationExtension, macOS, macOSApplicationExtension, macCatalyst, macCatalystApplicationExtension, watchOS, tvOS です。 * を指定することで、その他を指定できます。
プラットフォームバージョンは、数値、数値.数値、数値.数値.数値 のいずれかです。

ですので、#available(macOS 12) というような指定が可能になります。
なお、カンマで区切ることで複数の条件を記述することができますが、&& や || を使用して複数の条件を複合させることはできません。

実際には、#available(macOS 12) と書くと、「* を追加して将来的なプラットフォーム拡張に対応しなさい」というエラーとなり、#available(macOS 12, *) と書くことが必要となります。以降での説明も同様です。

MEMO
少しオフトピックですが、SwiftPackage を作成する時には、iOSApplicationExtension であるかに気をつけることが必要になるケースがあります。
SwiftPackageManagerEyeCatch[SwiftPM][iOS] ‘shared’ is unavailable in application extensions for iOS の対処法

条件 * の意味

#if を使った conditional compilation block と異なるのは、#available は、コンパイル対象のプラットフォームについての指示がないとコンパイルエラーになることです。


if #available(iOS 14) {
...
}

上記のコードは、iOS 向けにはコンパイルできますが、macOS 上でコンパイルしようとするとエラーになります。
エラーは、「Condition required for target platform 'macOS'」というものです。
文字通り、macOS についての制約が見つからないためにエラーとなっています。

解決策としては、macOS についても具体的な条件を指定するか、無指定にするかです。無指定時には、* を使って表現しますが、常に true として扱われます。


if #available(iOS 14, macOS 11, *) { // iOS14 以降と macOS 11 以降
...
}

if #available(iOS 14, *) { // iOS14 以降と macOS であれば全て
...
}

@availeble

#available に似たものに、@available というのもみます。

@available は、2種類あります。

クラスやメソッドに付与する @available

クラスやメソッドに @available を付与することで、使用可能なプラットフォームを宣言することができます。


@available(iOS 11, macOS 10.13, *)
func newMethod() {
    // Use iOS 11 APIs.
}

上記の newMethod は、iOS 11 以降 macOS 10.13 以降でのみ使えるということを宣言しています。

つまり 使う側では、以下のように書いている(かもしれない)ことになります。
# アプリのサポート OS 次第です。


if #available(iOS11, macOS 10.13, *) {
    newMethod()
}

#available と同じ context で使用される @available

#available と同様の context 例えば if の 条件節で使用されている @available は、#available の Objcective-C 版です。


// Objective-C
if (@available(iOS 11, *)) {
    // Use iOS 11 APIs.
} else {
    // Alternative code for earlier versions of iOS.
}

// equivalent in Swift
if #available(iOS 11, *) {
    // Use iOS 11 APIs.
} else {
    // Alternative code for earlier versions of iOS.
}

Apple のドキュメントは、こちら

Swift の @available とは使い方が異なるので、混乱の元になる気がします・・・

まとめ

#if os(), #available(), @available() の使い方をまとめてみました。

#if os(), #available(), @available() の使い方
  • #if は、コンパイル対象とするかどうかを切り替える
  • #available は、動作環境を確認して切り替える
  • @available は、対象環境を宣言する
  • @available は、Objective-C と Swift のどちらにも存在するが意味が異なる。

説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。

コメントを残す

メールアドレスが公開されることはありません。