Unavailability Condition
01 何が問題だったのか
Swift では、ある API が特定の OS バージョン以降でしか使えないときに、#available を使って利用可能かどうかを判定できます。しかし、「特定のバージョン 未満 の場合だけ実行したい」という逆方向のチェックは素直に書けず、不自然な回避策が必要でした。
負の可用性チェックが必要になる場面
iOS 13 で UIWindow の初期化タイミングが AppDelegate から SceneDelegate に変わったケースのように、API の違いが大きく if / else の片側に押し込められない場合、「iOS 13 未満 の場合だけ旧来の初期化コードを動かす」ような条件が書きたくなります。
// iOS 13 未満のときだけ window をロードする。
// iOS 13 以降は SceneDelegate 側で後からロードされる。
if #available(iOS 13, *) {
} else {
loadMainWindow()
}
deprecated になった API の扱いでも同じ状況が起こります。たとえば ASIdentifierManager.isAdvertisingTrackingEnabled は iOS 14 で非推奨となりましたが、古い iOS では従来どおり動作します。新しい OS では App Tracking Transparency のアラートを表示したくないのでその機能自体をやめたい、という場合、「iOS 14 未満 のときだけ従来のトラッキング処理を走らせる」チェックが必要になります。
#available を否定できないことによる回避策の問題
#available はブール式ではないため、!#available(...) や #available(...) == false のように否定することができません。そのため、次のように else を使う回避策が必要でした。
if #available(iOS 13, *) {} else {
loadMainWindow()
}
この書き方には次のような問題があります。
- 空の
ifブランチは普通はバグの兆候なので、意図的であることを説明するコメントが必須になります。リンターがこれを誤りとみなして修正提案を出してしまうこともあります。 - 一方で空ブランチを隠そうと
ifとelseを 1 行にまとめると、今度は通常の可用性チェックと見分けがつかず、コードレビューで見落とされやすくなります。 guard #available(iOS 13, *) else { ... }の形にするとより簡潔ですが、guardのガード節は happy path を残すためのものという一般的なコードスタイルに反します。負の可用性チェックでは、むしろガード節側が happy path になってしまいます。
なお Objective-C では if (@available(iOS 13.0, *) == NO) のように書けるため、この不便は Swift 固有のものです。UIScenes に対応する iOS アプリを書くたびにこの回避策を書かなければならない状況があり、API の追加・変更が複数のバージョンにまたがる場合には同様の問題が繰り返し発生していました。
02 どのように解決されるのか
#available の反対の意味を持つ新しい条件 #unavailable が導入されます。これにより、負の可用性チェックを空の if ブランチや guard を使わずに自然に書けるようになります。
if #unavailable(iOS 13, *) {
loadMainWindow()
}
名前のとおり #unavailable は #available の反転版で、#available(iOS 13, *) が false になる場面でこちらは true になります。回避策のような空ブランチやコメントは不要で、「iOS 13 未満で実行したい」という意図がコードからそのまま読み取れます。
シンボル可用性の扱い
#available と同様に、#unavailable もスコープ内で使える API のバージョンを引き上げます。ただし引き上げられるのは else 節の側です。
if #unavailable(iOS 13, *) {
// シンボル可用性: デフォルト(デプロイメントターゲット)
} else {
// シンボル可用性: iOS 13
}
else if が複数ある場合は、そのすべての分岐で可用性が引き上げられます。
if #unavailable(iOS 9.0, *) {
// シンボル可用性: デフォルト(デプロイメントターゲット)
} else if a == b {
// シンボル可用性: iOS 9.0
} else if b == c {
// シンボル可用性: iOS 9.0
} else {
// シンボル可用性: iOS 9.0
}
#available と #unavailable の混在は禁止
同一の if 文の中で #available と #unavailable を並べることは禁止されます。たとえば次のようなコードでは、else 節のシンボル可用性がどちらの条件が偽かによって変わってしまい、コンパイラが一意に決められません。
if #available(iOS 9.0, *), #unavailable(iOS 13.0, *)
// error: #available and #unavailable cannot be in the same statement
必要であれば、次のように文を分けてネストすれば曖昧さなく書けます。
if #available(iOS 9.0, *) {
// シンボル可用性: iOS 9.0
if #unavailable(iOS 13.0, *) {
// シンボル可用性: iOS 9.0
} else {
// シンボル可用性: iOS 13.0
}
} else {
// シンボル可用性: デフォルト(デプロイメントターゲット)
}
ワイルドカード * の意味
可用性のスペックリストはブール式ではなく、コンパイル中のプラットフォームに一致するエントリ 1 件だけを見て真偽を決めます。* は「スペックリストに明示されていないプラットフォーム」を表すワイルドカードで、リスト内に対応プラットフォームのエントリが無いときの値を、そのプラットフォームのデプロイメントターゲットとして扱うための仕組みです。
この意味論は #unavailable でも同じです。コンパイル中のプラットフォームが明示されていない場合、#unavailable(*) はデプロイメントターゲット未満かどうかを聞いていることになり、常に false になります。
if #unavailable(*) {
// 到達しない
} else {
// ...
}
この副作用として、複数の #unavailable を 1 文に並べて別プラットフォームを扱うと、ワイルドカードが常に false 側に倒れて文全体が到達不能になります。
if #unavailable(iOS 13, *), #unavailable(watchOS 3, *) {
// この分岐には到達しない
}
このようなケースでは「1 つの #unavailable にまとめる」ように、コンパイラが診断と fix-it を提示します。
if #unavailable(iOS 13, watchOS 3, *) {
// OK
}
result builder との関係
#unavailable は #available と同じ仕組みで動くため、ViewBuilder などの result builder 側を変更する必要はありません。#unavailable を使ったときは、else 節の側で buildLimitedAvailability(_:) が呼ばれます(SE-0289 参照)。
既存の書き方からの移行
!#available(...) や #available(...) == false という書き方は今回のスコープ外ですが、コンパイラはこうした書き方に対して #unavailable(...) への fix-it を提示するので、既存コードからの移行は容易です。