#if vs @available vs if #available

What are the differences between #if, @available, and if #available, in terms of both compilation and use case?

Here is my understanding so far:

If I want to make a function available on macOS only, I can do it like this:

#if os(macOS)
func someMacOSOnlyFunction() { /* ... */ }
#endif

If I want to restrict it even further to some specific versions, I can do either of these 2:

@available(macOS 10.15, *)
func someMacOS15PlusOnlyFunction() { /* ... */ }
if #available(macOS 10.15, *) {
    func someMacOS15PlusOnlyFunction() { /* ... */ }
}

I know that the compiler ignores everything in #if, if the condition is not met. Does the compilation work likewise for @available and if #available? Does @available behave differently from if #available in any way? What are the best practices for choosing/using them?

#if is a compile-time condition. The variables are evaluated at compile time, and then whichever branch is taken is the only one investigated. The code in non-taken blocks does not have to compile or type check: it is as though it were not there at all.

@available is an attribute that applies to a subset of Swift declarations, including types, methods, functions, and properties. When used in the way you show above it restricts the availability of the declaration to a specific minimum version on the listed platforms. For non-Apple SDKs this has one major effect: within the scope of the associated declaration the minimum deployment target can be assumed to be the version specified in the availability annotation, not the one used at compile time. This allows that declaration to use newer types, functions, methods, and so-on.

This takes effect at compile time as well. However, unlike with #available, the method must type check and compile. The code will always be emitted into your binary: however, it will only be used when the binary is executed on platforms that meet the availability requirements.

if #available is a run-time version check. At run-time, the condition will be evaluated. If met, code execution will enter the block, otherwise the else case will execute. Like @available, this also has the effect of raising the effective minimum deployment target within the if block to the version specified in the condition. This means that both branches must compile and type-check.

6 Likes

Note that this means that in general you cannot guard declarations with #if blocks because declarations are usually at file-scope, not in functions.

1 Like

What does "emit" mean here?

emit means that there will be assembly code (or bitcode, for store submission) directly corresponding to the code you wrote in both branches.

How do you pronounce them (if speaking them out loud)?

I don't know that there's a single definitive pronunciation. I usually say "pound-if" or "compiler directive" for #if, "if available" or "runtime version check" for if #available, and "availability annotation" or "at-available" for @available. But anything that gets the idea across is sufficient.

I pronounce "#" as "pound", and, at least in this context, I don't pronounce the "@" at all.

I have 2 follow-on questions:


Say if I have this declaration:

protocol ExampleProtocol {}

@available(macOS 10.12)
extension DateInterval: ExampleProtocol {}

and try to run it on macOS 10.11. The type DateInterval isn't available on macOS 10.11. How does the compiler type check and compile it when it can't find the symbols?


By "#available", do you mean #if or if #available? I assume it's #if, because it seems to me that if the block of if #available needs to be executable, then it needs to be type-checked and compiled.

When thinking about all of these it is critical to consider the distinction between compile time and run time. The above code will compile when compiled on any system that has an SDK new enough to contain the DateInterval type in the SDK. This will have shipped with the Xcode that shipped with macOS 10.12, or any later Xcode.

The compiled artefact will run on macOS 10.11 (if your minimum deployment target is configured to include 10.11), but the DateInterval code will not be executed. It will still exist in the binary, but you will have been required to introduce runtime checks that prevent that code from ever running as part of the build process.

This is an important thing to note: the minimum SDK version you require at compile time is distinct from the minimum deployment target you set for runtime. The first constrains the build system, the second constrains the use system. The minimum deployment target is a configuration of the project, and can never be higher than the minimum SDK version you require. The minimum SDK version is implicit, and is derived from the highest of:

  • the minimum deployment target
  • all @available or if #available guards in your program

Correct.

3 Likes