Swift API Availability
Code exists in a world of infinite abundance. Whatever you can imagine is willed into being — so long as you know how to express your desires.
As developers, we know that code will eventually be compiled into software, and forced to compete in the real-world for allocation of scarce hardware resources. Though up until that point, we can luxuriate in the feeling of unbounded idealism… well, mostly. For software is not a pure science, and our job — in reality — is little more than shuttling data through broken pipes between leaky abstractions.
This week on NSHipster,
we’re exploring a quintessential aspect of our unglamorous job:
API availability.
The good news is that Swift provides first-class constructs
for dealing with these real-world constraints
by way of @available
and #available
.
However,
there are a few nuances to these language features,
of which many Swift developers are unaware.
So be sure to read on
to make sure that you’re clear on all the options available to you.
@available
In Swift,
you use the @available
attribute
to annotate APIs with availability information,
such as
“this API is deprecated in macOS 10.15” or
“this API requires Swift 5.1 or higher”.
With this information,
the compiler can ensure that any such APIs used by your app
are available to all platforms supported by the current target.
The @available
attribute can be applied to declarations,
including
top-level functions, constants, and variables,
types like structures, classes, enumerations, and protocols,
and type members —
initializers, class deinitializers, methods, properties, and subscripts.
Platform Availability
When used to designate platform availability for an API,
the @available
attribute can take one or two forms:
- A “shorthand specification” that lists minimum version requirements for multiple platforms
- An extended specification that can communicate additional details about availability for a single platform
Shorthand Specification
@available(platform version , platform version ..., *)
- A platform;
i
,OS mac
,Catalyst mac
/OS OSX
,tv
, orOS watch
, or any of those withOS Application
appended (e.g.Extension mac
).OSApplication Extension - A version number
consisting of one, two, or three positive integers,
separated by a period (
.
), to denote the major, minor, and patch version. - Zero or more versioned platforms in a comma-delimited (
,
) list. - An asterisk (
*
), denoting that the API is available for all other platforms. An asterisk is always required for platform availability annotations to handle potential future platforms (such as the long-rumoredi
).Dishwasher OS
For example, new, cross-platform APIs introduced at WWDC 2019 might be annotated as:
@available(mac OS 10.15, i OS 13, watch OS 6, tv OS 13, *)
Shorthand specifications are a convenient way to annotate platform availability. But if you need to communicate more information, such as when or why an API was deprecated or what should be used as a replacement, you’ll want to opt for an extended specification instead.
Introduced, Deprecated, Obsoleted, and Unavailable
// With introduced, deprecated, and/or obsoleted
@available(platform | *
, introduced: version , deprecated: version , obsoleted: version
, renamed: "..."
, message: "...")
// With unavailable
@available(platform | *, unavailable , renamed: "..." , message: "...")
- A platform, same as before, or an asterisk (*) for all platforms.
- Either
introduced
,deprecated
, and/orobsoleted
…- An
introduced
version, denoting the first version in which the API is available - A
deprecated
version, denoting the first version when using the API generates a compiler warning - An
obsoleted
version, denoting the first version when using the API generates a compiler error
- An
- …or
unavailable
, which causes the API to generate a compiler error when used -
renamed
with a keypath to another API; when provided, Xcode provides an automatic “fix-it” - A
message
string to be included in the compiler warning or error
Unlike shorthand specifications,
this form allows for only one platform to be specified.
So if you want to annotate availability for multiple platforms,
you’ll need stack @available
attributes.
For example,
here’s how the previous shorthand example
can be expressed in multiple attributes:
@available(mac OS, introduced: 10.15)
@available(i OS, introduced: 13)
@available(watch OS, introduced: 6)
@available(tv OS, introduced: 13)
Apple SDK frameworks make extensive use of availability annotations to designate new and deprecated APIs with each release of iOS, macOS, and other platforms.
For example,
iOS 13 introduces a new
UICollection
class.
If you jump to its declaration
by holding command (⌘) and clicking on that symbol in Xcode,
you’ll see that it’s annotated with @available
:
@available(i OS 13.0, *)
open class UICollection View Compositional Layout : UICollection View Layout { … }
This @available
attribute tells the compiler that
UICollection
can only be called
on devices running iOS, version 13.0 or later
(with caveats; see note below).
If your app targets iOS 13 only,
then you can use UICollection
without any special consideration.
If, however,
your deployment target is set below iOS 13
in order to support previous versions of iOS
(as is the case for many existing apps),
then any use of UICollection
must be conditionalized.
More on that in a moment.
Swift Language Availability
Your code may depend on a new language feature of Swift,
such as property wrappers
or default enumeration case associated values —
both new in Swift 5.1.
If you want to support development of your app with previous versions of Xcode
or for your Swift package to work for multiple Swift compiler toolchains,
you can use the @available
attribute
to annotate declarations containing new language features.
When used to designate Swift language availability for an API,
the @available
attribute takes the following form:
@available(swift version)
Unlike platform availability,
Swift language version annotations
don’t require an asterisk (*
);
to the compiler, there’s one Swift language,
with multiple versions.
#available
In Swift,
you can predicate if
, guard
, and while
statements
with an availability condition,
#available
,
to determine the availability of APIs at runtime.
Unlike the @available
attribute,
an #available
condition can’t be used for Swift language version checks.
The syntax of an #available
expression
resembles that of an @available
attribute:
if | guard | while #available(platform version , platform version ..., *) …
Now that we know how APIs are annotated for availability, let’s look at how to annotate and conditionalize code based on our platform and/or Swift language version.
Working with Unavailable APIs
Similar to how, in Swift, thrown errors must be handled or propagated, use of potentially unavailable APIs must be either annotated or conditionalized code.
When you attempt to call an API that is unavailable for at least one of your supported targets, Xcode will recommend the following options:
- “Add
if #available
version check” - “Add
@available
attribute to enclosing declaration” (suggested at each enclosing scope; for example, the current method as well as that method’s containing class)
Following our analogy to error handling,
the first option is similar to prepending try
to a function call,
and the second option is akin to wrapping a statement within do/catch
.
For example,
within an app supporting iOS 12 and iOS 13,
a class that subclasses UICollection
must have its declaration annotated with @available
,
and any references to that subclass
would need to be conditionalized with #available
:
@available(i OS 13.0, *)
final class Custom Compositional Layout: UICollection View Compositional Layout { … }
func create Layout() -> UICollection View Layout {
if #available(i OS 13, *) {
return Custom Compositional Layout()
} else {
return UICollection View Flow Layout()
}
}
Swift comprises many inter-dependent concepts, and crosscutting concerns like availability often result in significant complexity as various parts of the language interact. For instance, what happens if you create a subclass that overrides a property marked as unavailable by its superclass? Or what if you try to call a function that’s renamed on one platform, but replaced by an operator on another?
While it’d be tedious to enumerate every specific behavior here, these questions can and often do arise in the messy business of developing apps in the real world. If you find yourself wondering “what if” and tire of trial-and-error experimentation, you might instead try consulting the Swift language test suite to determine what’s expected behavior.
Alternatively, you can surmise how things work generally from Clang’s diagnostic text:
-Wavailability
Diagnostic Text
-Wavailability
Diagnostic Textwarning: | ‘unavailable’ availability overrides all other availability information | |||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
warning: | unknown platform A in availability macro | |||||||||||||
warning: | feature cannot be |
|
in B version C before it was |
|
in version E ; attribute ignored | |||||||||
warning: | use same version number separators ‘_’ or ‘.’; as in ‘major[.minor[.subminor]]’ | |||||||||||||
warning: | availability does not match previous declaration | |||||||||||||
warning: |
|
method |
|
|
on
B ( C vs. D ) |
|||||||||
warning: |
|
method cannot be unavailable on A when |
|
is available |
Annotating Availability in Your Own APIs
Although you’ll frequently interact with @available
as a consumer of Apple APIs,
you’re much less likely to use them as an API producer.
Availability in Apps
Within the context of an app,
it may be convenient to use @available
deprecation warnings
to communicate across a team
that use of a view controller, convenience method, or what have you
is no longer advisable.
@available(i OS, deprecated: 13, renamed: "New And Improved View Controller")
class Old View Controller: UIView Controller { … }
class New And Improved View Controller: UIView Controller { … }
Use of unavailable
or deprecated
, however,
are much less useful for apps;
without any expectation to vend an API outside that context,
you can simply remove an API outright.
Availability in Third-Party Frameworks
If you maintain a framework that depends on the Apple SDK in some way, you may need to annotate your APIs according to the availability of the underlying system calls. For example, a convenience wrapper around Keychain APIs would likely annotate the availability of platform-specific biometric features like Touch ID and Face ID.
However,
if your framework wraps SDK functionality
in a way that doesn’t expose such implementation details,
you might be able to opt-in to new features
without affecting the availability for your own APIs.
For example,
an NLP
library that delegates functionality to
NSLinguistic
could instead use
Natural Language framework
when available
(as determined by #available
),
without any user-visible API changes.
Availability in Swift Packages
If you’re writing Swift qua Swift
in a platform-agnostic way
and distributing that code as a Swift package,
you may want to use @available
to give a heads-up to consumers about APIs that are on the way out.
Unfortunately, there’s currently no way to designate deprecation in terms of the library version (the list of platforms are hard-coded by the compiler). While it’s a bit of a hack, you could communicate deprecation by specifying an obsolete / non-existent Swift language version like so:
@available(swift, deprecated: 0.0.1, message: "Deprecated in 1.2.0")
func going(going: Gone...) {}
Working Around Deprecation Warnings
As some of us are keenly aware,
it’s not currently possible to silence deprecation warnings in Swift.
Whereas in Objective-C,
you could suppress warnings with #pragma clang diagnostic push / ignored / pop
,
no such convenience is afforded to Swift.
If you’re among the l33t coders who have “hard mode” turned on
(“Treat Warnings as Errors” a.k.a. SWIFT_TREAT_WARNINGS_AS_ERRORS
),
but find yourself stymied by deprecation warnings,
here’s a cheat code you can use:
class Custom View {
@available(i OS, introduced: 10, deprecated: 13, message: "😪")
func method() {}
}
Custom View().method() // "'method()' was deprecated in i OS 13: 😪"
protocol Ignoring Method Deprecation {
func method()
}
extension Custom View: Ignoring Method Deprecation {}
(Custom View() as Ignoring Method Deprecation).method() // No warning! 😁
As an occupation, programming epitomizes the post-scarcity ideal of a post-industrial world economy in the information age. But even so far removed from physical limitations, we remain inherently constrained by forces beyond our control. However, with careful and deliberate practice, we can learn to make use of everything available to us.