Swift Regex Deep Dive
iOS MacOur introductory guide to Swift Regex. Learn regular expressions in Swift including RegexBuilder examples and strongly-typed captures.
Swift 2.0 introduced a new language construct, #available
, that helps solve
the problems that crop up when your app needs to support multiple versions of
iOS or OS X.
Using some API that’s available only in iOS 9? The Swift availability
features prevent you from trying to run that code when the app is running on iOS 8.
But first: there’s a common misconception that #available
is used for including or excluding code at compile time. Given the name, it’s reasonable to think, “This call is available only on watchOS, so this extension I’m writing for iOS
shouldn’t include that code at all because it’s not available.”
#available
is the
wrong tool for this. Code in #available
clauses always compile. You’ll want to
use the #if
build configuration statement instead.
Apple likes to update their operating systems on a regular basis—new features
get added, new APIs are introduced and older crufty APIs are occasionally
deprecated. Except for a few specific deviations in the past (like ARC),
Apple never back-ports new stuff to older OSes.
As app developers, we don’t have the luxury of shipping our software exclusively on the
latest-and-greatest OS version. We want to use the new shiny toys, but we also
need to be able to work on older versions of the OS that don’t have these
features. When you ship an app, you have a chunk of executable code for each
chip architecture you’re supporting, such as armv7 or arm64. You don’t have
separate chunks of executable code for different platform versions. The same
code will run on iOS 9, iOS 8 and as far back in the OS catalog that
you want to support.
There are two OS version numbers that are involved when building software in the
Apple ecosystem.
The first is the Target SDK version. SDK stands for “Software Development Kit,”
which is the set of libraries and headers for a particular OS version. This is
the version of Apple’s APIs that you compile and link against. The SDK describes
the set of
API available to you. Linking against the iOS 9 SDK means you can use
any API that comes with iOS 9. You won’t be able to directly use stuff introduced in
iOS 10. Modern Xcodes are tightly coupled to the SDKs for the latest OS
versions, so if you upgrade your Xcode, you will be linking against a newer version
of the SDK.
The other version number is the Deployment Target. This declares
the oldest OS version your app will support. How far back you decide to support
is a business decision based on how much work you are willing to do for customers on
older versions of the OS.
So, a modern App might use iOS 9 as the Target SDK, and iOS 7 as the deployment
target. This means that you can run on iOS 7, iOS 8 and iOS 9, and that you
have available to you any iOS 9 calls when actually running on iOS 9.
There’s a problem, though. What happens if you use a new iOS 9 class, such as
NSDataAsset
, but your user is running iOS 8? There is no NSDataAsset
in the libraries that shipped with their iPad, so your app won’t work. Depending on
language and circumstances, the behavior of using newer API on older systems
could range from silently not doing what you want all the way to immediate
process termination.
The solution to the problem is to never enter code paths intended for newer
versions of the OS.
Well, that was easy. Time for lunch!
While the solution is easy to describe, it is harder to implement. Back in the
old days, we’d do a lot of work explicitly checking that a particular feature
was there before using it. Even then, improper API use would still slip through
to shipping apps.
Why is the solution harder to implement? It turns out there actually two
problems that need to be solved. The first happens at compile time: I want to
know if I’m accidentally using API that may be too fresh for my deployment target. It’s much
better to catch errors at compile time.
The other problem happens at run time: you need to construct your program logic so
that you decide whether or not to enter the code paths that use newer API.
Swift 2’s availability system addresses both of these problems. It’s a
conspiracy between the compiler, to detect the use of API that’s “too new” for the
code’s current context, and at run time to query the OS version and
conditionally skip over (or enter) chunks of code based on availability of API.
You can think of a sequence of code as having an “SDK Level” (not an official
term, but it’s how I envision this stuff). This is the version of the SDK that
the programmer was using as their baseline for what API is available.
There is no problem if you use calls that were introduced in, or are older than, this level.
There is no need to guard its usage. If you use calls that were introduced in a newer
version, however, you will need to tell the compiler, “Hey, this chunk of code uses iOS 9
goodies.”
By default, code has an ambient SDK level dictated by the deployment target.
API older than this SDK is ok. API newer than this SDK could cause problems.
The compiler will reject any API usage that’s too new, unless you tell the
compiler to stop complaining.
You do that by using the availability condition, affectionately known as
#available
. You specify an OS version in a conditional statement and the
compiler will then raise its idea of the current SDK level until that scope is
exited.
Here’s some new code just written for a project that targets iOS 7.
NSDataAsset
is not available in iOS 7:
func fetchKittyDescriptions() -> [String]? {
let dataAsset = NSDataAsset(name: "Hoover")
return nil
}
The compiler knows that NSDataAsset
was introduced in iOS 9, so it gives you an error:
Notice that this is an error. You must address this. This is default
behavior, and there is no way to opt-out. Otherwise the
compiler refuses to build your app, because it would lead to terrible horrible
death at runtime on iOS 7 targets. Remember that one of Swift’s stated goals is
to make things as safe as possible at compile time.
Xcode’s editor offers a fix-it:
Choosing the first option yields code like this:
func fetchKittyDescriptions() -> [String]? {
if #available(iOS 9.0, *) {
let dataAsset = NSDataAsset(name: "Hoover")
} else {
// Fallback on earlier versions
}
return nil
}
The #available
statement does two things. Firstly, at compile time, it raises the SDK
level for the true portion of the if
to iOS 9.0 (from the ambient iOS 7 deployment
target). Any API calls from iOS 9 are legal inside of that set of braces. The
else
portion still has the ambient SDK level, which is where you’d put in an
implementation that uses only older API, or punts on the feature entirely.
Secondly, at run time, #available
in that conditional is querying the OS version. If
the currently running iOS version is 9 or beyond, control enters the first part of the if statement. If it’s 8
or lower, it jumps to the else
part.
By combining this compile-time and run-time check, we’re guaranteed to always
safely use API.
#available
works with if
statements, while
statements, and it also works with guard
:
func fetchKittyDescriptions() -> [String]? {
guard #available(iOS 9.0, *) else { return nil }
...
}
This raises the SDK level of the function after the guard statement.
The code above uses #available
in a reactive way: “This one time in this
function, I want to temporarily raise my SDK level so I can get my code compiling
again”. This can become obnoxious if you use latest-version API all over the place
in a particular function or class. Use the @available
attribute to decorate
individual functions or entire classes to say, “OK world, all this stuff
is based on iOS 9 functionality.”:
@available(iOS 9.0, *)
func fetchKittyDescriptions() -> [String]? {
let dataAsset = NSDataAsset(name: "Hoover")
print ("(dataAsset)")
return nil
}
This marks the entire function as having an SDK level of iOS 9. You can call iOS
9 API in here all day and not have any complaints about using
too-new API. The compiler keeps a record of the SDK level for all the code it
encounters, so if you have some code that’s using the ambient SDK level (iOS 7
in this example) that tries to call fetchKittyDescriptions
without an availability guard, you’ll get an error:
You can raise the SDK level for classes, structs and enums with @available.
So, about that syntax inside of the parens:
#available(iOS 9.0, *)
It’s simply a list of applicable platform names (iOS, OS X, watchOS, tvOS and
each of those names suffixed by ApplicationExtension), paired with a version
number.
You can declare a chunk of code that needs the latest of everything:
func fetchKittyDescriptions() -> [String]? {
if #available(iOS 9, OSX 10.11, watchOS 2.0, tvOS 9.0, *) {
let dataAsset = NSDataAsset(name: "Hoover")
print ("(dataAsset)")
}
return nil
}
So what’s the deal with the star at the end? That’s required. If you leave it
off, the compiler will complain:
You’ll see “Must handle potential future platforms with *“. That means that code will be
treated as having a particular ambient SDK level if a platform is not explicitly supplied. If fetchKittyDescriptions
gets included in an app destined for Apple’s new toasterOS,
the availability of NSDataAsset
will be judged against the ambient toasterOS SDK,
which will be the toaster deployment target.
Remember that availability is not #if
. This language construct does not control
whether the code will get compiled for toasterOS. If this code is in a toasterOS
project, it will get compiled. What OS version does the compiler use to decide
if NSDataAsset
is available? The deployment target.
The star is there to remind anyone reading the code that the list of platforms
listed in the #available
conditional is not exhaustive. In the future, there
will be new platforms, and the code you’re looking at might be compiled for
them.
The availability features in the compiler and at run time are nice, and they
work well. Be vigilant when using Xcode’s fix-its, though. In the writing of
this, I witnessed two different cases of code corruption. The first left a copy
of the iOS 9 calls outside of the if #available check:
Aside from the bad indentation, I got another availability
error. “But… but… you just fixed that!”
The second actually deleted legitimate code. Notice that the return statement
and closing brace are deleted outright, with no undo.
I can’t trust tools that will destroy code like this, so I manually add
#available
and @available
statements when needed. The compiler error tells you
what version to put in to the availability check.
When the new availability model was announced this year, many old-timers (myself
included) immediately shouted at the video stream, “But you’ve been telling us
for FIFTEEN YEARS never to do OS version comparisons!”
And that’s true. The guidance in Objective-C land had been to explicitly test
for availability of newer features before you used them.
For a number of reasons, I never liked that model. There is no unified
mechanism for checking for feature availability. There are a bunch of
different things you had to do depending on the type of API feature that you
wanted to see if it was available, such as whether a particular class exists
(thanks to weak linking, it’ll be nil in Objective-C if it doesn’t exist), or
whether an object responds to a
particular selector, or if a particular function pointer is not-NULL.
It’s also tedious and bug-prone because Xcode doesn’t have tooling in Objective-C
to tell us we
are potentially using new API on older systems. We’d unknowingly use API that
was too new, and then blow up at run-time on older devices.
There was also the possibility of false positives. What if the class does exist
and you’re not supposed to use it yet, like what happened with UIGestureRecognizer
? If a constant is pointing to an object, what might it mean for it to be NULL? What does it mean for a global float constant or enum value to not exist, given that
the compiler can inline it if it knows the value?
The take-away points: #available
is not #if
—it is not conditional compilation
based on OS platform. Instead, at compile time, #available
tells the compiler
to temporarily raise the SDK level above your deployment target so that you can
safely use newer API. At run time, the code will execute depending on the version
of the OS the app is running on.
Our introductory guide to Swift Regex. Learn regular expressions in Swift including RegexBuilder examples and strongly-typed captures.
The Combine framework in Swift is a powerful declarative API for the asynchronous processing of values over time. It takes full advantage of Swift...
SwiftUI has changed a great many things about how developers create applications for iOS, and not just in the way we lay out our...