Sponsor Link
環境&対象
- 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 向けにコンパイルするとエラーになってしまうからです。
似ているけれども異なるユースケースがあります。
最新の 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)
使用可能文字は、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, *) と書くことが必要となります。以降での説明も同様です。
少しオフトピックですが、SwiftPackage を作成する時には、iOSApplicationExtension であるかに気をつけることが必要になるケースがあります。
[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 は、コンパイル対象とするかどうかを切り替える
- #available は、動作環境を確認して切り替える
- @available は、対象環境を宣言する
- @available は、Objective-C と Swift のどちらにも存在するが意味が異なる。
説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。
SwiftUI おすすめ本
SwiftUI を理解するには、以下の本がおすすめです。
SwiftUI ViewMatery
SwiftUI で開発していくときに、ViewやLayoutのための適切なmodifierを探すのが大変です。
英語での説明になってしまいますが、以下の”SwiftUI Views Mastery Bundle”という本がビジュアル的に確認して探せるので、便利です。
英語ではありますが、1ページに コードと画面が並んでいるので、非常にわかりやすいです。
View に適用できる modifier もわかりやすく説明されているので、ビューの理解だけではなく、どのような装飾ができるかも簡単にわかります。
超便利です
販売元のページは、こちらです。
SwiftUI 徹底入門
# SwiftUI は、毎年大きく改善されていますので、少し古くなってしまいましたが、いまでも 定番本です。
Swift学習におすすめの本
詳解Swift
Swift の学習には、詳解 Swift という書籍が、おすすめです。
著者は、Swift の初期から書籍を出していますし、Swift の前に主力言語だった Objective-C という言語についても同様の書籍を出しています。
最新版を購入するのがおすすめです。
現時点では、上記の Swift 5 に対応した第5版が最新版です。
Sponsor Link