With every release of iOS, Apple introduces new frameworks and technologies and gets rid of others. These changes are always exciting to users — but they can be a real headache for developers. Availability attributes in Swift help give developers relief from these headaches.
Although most users adopt new versions of iOS quite quickly, it is still important to make sure your apps work on previous versions. Apple recommends supporting one system version back, meaning that when iOS 10 is released this fall, you should still support iOS 9.3.
But what if you want to add a feature that is only available in the newest version, or you need to use a deprecated API on older versions?
That is where Swift availability attributes come in. These attributes make it easy for you to ensure your code works on every system version you support.
The best way to understand availability is to get your hands dirty with some code. Let’s dive in!
Note: You will need Xcode 8 or above to work through this tutorial.
Getting Started
Download the starter project and open Persona.xcodeproj. Persona is an app that shows random famous people from history, from ancient emperors to musical artists.
Persona is built using the Contacts framework, which was first introduced in iOS 9. This means that Persona only works on iOS 9 or greater. Your task is to add support for iOS 8.4. Talk about #throwbackthursday! :]
First, examine the project. In the Project navigator, there are two groups, vCards and Images. For each person in the app, there is a .vcf and .jpg image to match.
Open PersonPopulator.swift. PersonPopulator
has a class method generateContactInfo()
, which chooses a random person and returns the contact and image data.
Next, open the Persona group and move to ViewController.swift. Each time the user taps the “Random” button, getNewData()
is called and repopulates the data with the new person.
Supporting iOS 8.4 with Availability Attributes
This app currently supports only iOS 9.3 and above. However, supporting iOS 8.4, the most recent version of iOS 8, would be ideal.
Setting the oldest compatible version of iOS is simple. Go to the General pane in Persona’s iOS target settings. Find the Deployment Info section and set Deployment Target to 8.4:
After making this change, build the project; it looks like things are broken already.
Xcode shows two errors in PersonPopulator:
In order to fix this error, you need to restrict generateContactInfo()
to certain iOS versions — specifically, iOS 9 and greater.
Adding Attributes
Open PersonPopulator.swift and add the following attribute right above generateContactInfo()
:
@available(iOS 9.0, *) |
This attribute specifies that generateContactInfo()
is only available in iOS 9 and greater.
Checking the Current Version
Now that you’ve made this change, build the project and notice the new error in ViewController.swift.
The new error states that generateContactInfo()
is only available on iOS 9.0 or newer, which makes sense because you just specified this condition.
To fix this error, you need to tell the Swift compiler that this method will only be called in iOS 9 and above. You do this using availability conditions.
Open ViewController.swift and replace the contents of getNewData()
with the following:
if #available(iOS 9.0, *) { print("iOS 9.0 and greater") let (contact, imageData) = PersonPopulator.generateContactInfo() profileImageView.image = UIImage(data: imageData) titleLabel.text = contact.jobTitle nameLabel.text = "\(contact.givenName) \(contact.familyName)" } else { print("iOS 8.4") } |
#available(iOS 9.0, *)
is the availability condition evaluated at compile time to ensure the code that follows can run on this iOS version.
The else
block is where you must write fallback code to run on older versions. In this case, the else
block will execute when the device is running iOS 8.4.
Build and run this code on the iPhone simulator running iOS 9.0 or greater. Each time you click “Random”, you’ll see iOS 9.0 and greater
printed to the console:
Adding Fallback Code
The Contacts framework introduced in iOS 9 replaced the older Address Book framework. This means that for iOS 8.4, you need to fall back to the Address Book to handle the contact information.
Open PersonPopulator.swift and add the following line to the top of the file:
import AddressBook |
Next, add the following method to PersonPopulator
:
class func generateRecordInfo() -> (record: ABRecord, imageData: Data) { let randomName = names[Int(arc4random_uniform(UInt32(names.count)))] guard let path = Bundle.main.path(forResource: randomName, ofType: "vcf") else { fatalError() } guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)) as CFData else { fatalError() } let person = ABPersonCreate().takeRetainedValue() let people = ABPersonCreatePeopleInSourceWithVCardRepresentation(person, data).takeRetainedValue() let record = NSArray(array: people)[0] as ABRecord guard let imagePath = Bundle.main.path(forResource: randomName, ofType: "jpg"), let imageData = try? Data(contentsOf: URL(fileURLWithPath: imagePath)) else { fatalError() } return (record, imageData) } |
This code does the same thing as generateContactInfo()
, but using the Address Book instead. As a result, it returns an ABRecord
instead of a CNContact
.
Because the Address Book was deprecated in iOS 9, you need to mark this method as deprecated as well.
Add the following attribute directly above generateRecordInfo()
:
@available(iOS, deprecated:9.0, message:"Use generateContactInfo()") |
This attribute lets the compiler know that this code is deprecated in iOS 9.0, and provides a message to warn you or another developer if you try to use the method in iOS 9 or greater.
Now it’s time to use this method.
Open ViewController.swift and add the following import statement to the top of the file:
import AddressBook |
Also, add the following to the else
block in getNewData()
:
print("iOS 8.4") let (record, imageData) = PersonPopulator.generateRecordInfo() let firstName = ABRecordCopyValue(record, kABPersonFirstNameProperty).takeRetainedValue() as! String let lastName = ABRecordCopyValue(record, kABPersonLastNameProperty).takeRetainedValue() as! String profileImageView.image = UIImage(data: imageData) titleLabel.text = ABRecordCopyValue(record, kABPersonJobTitleProperty).takeRetainedValue() as? String nameLabel.text = "\(firstName) \(lastName)" |
This code gets the random record info and sets the labels and the image the same way you have it in generateContactInfo()
. The only difference is instead of accessing a CNContact
, you access an ABRecord
.
Build and run the app on the simulator for iOS 9 or above, and everything will work as it did before. You will also notice that your app prints iOS 9.0 and greater
to the console:
However, the goal of everything you have done so far is to make Persona work on iOS 8.4. To make sure that this all worked, you need to try it out in the iOS 8.4 Simulator.
Go to Xcode/Preferences/Components, and download the iOS 8.4 Simulator.
When the simulator is finished downloading, select the iPhone 5 iOS 8.4 Simulator and click Run.
Persona runs the exact same way that it used to, but now it’s using the Address Book API. You can verify this in the console which says iOS 8.4
, which is from your code in the else
block of the availability conditional.
Availability for Cross-Platform Development
As if availability attributes weren’t cool enough already, what if I told you that they could make it much easier to reuse your code on multiple platforms?
Availability attributes let you specify the platforms you want to support along with the versions of those platforms you want to use. To demonstrate this, you’re going to port Persona to macOS.
First things first: you have to set up this new macOS target. Select File/New/Target, and under macOS choose Application/Cocoa Application.
Set the Product Name to Persona-macOS, make sure Language is set to Swift and Use Storyboards is selected. Click Finish.
Just like you added support for iOS 8.4, you need to support older versions of macOS as well. Select the macOS target and change the Deployment Target to 10.10.
Next, delete AppDelegate.swift, ViewController.swift, and Main.storyboard in the macOS target. In order to avoid some boilerplate work, download these replacement files and drag them into the project.
Note: When adding these files to the Xcode project, make sure the files are added to the Persona-macOS target, not the iOS target.
If Xcode asks if you want to set up an Objective C Bridging Header, click Don’t create.
So far, this target has the image view and two labels set up similar to the Persona iOS app, with a button to get a new random person.
One problem right now is that the images and vCards only belong to the iOS target — which means that your macOS target does not have access to them. This can be fixed easily.
In the Project navigator, select all the files in the Images and vCards folder. Open the File Inspector in the Utilities menu, and under Target Membership, check the box next to Persona-macOS:
You need to repeat this step again for PersonPopulator.swift, since your macOS app needs this file as well.
Now that you’ve completed the setup, you can start digging into the code.
Multi-Platform Attributes
Open PersonPopulator.swift. You may notice that the attributes all specify iOS, but there’s nothing about macOS — yet.
iOS 9.0 was released alongside with OS X version 10.11, which means that the new Contacts framework was also introduced on OS X in 10.11.
In PersonPopulator
above generateContactInfo()
, replace @available(iOS 9.0, *)
with the following:
@available(iOS 9.0, OSX 10.11, *) |
This specifies that generateContactInfo()
is first available on OS X 10.11, to match the introduction of the Contacts framework.
Note: Because OS X was renamed macOS in macOS 10.12 Sierra, Swift recently added macOS
as an alias for OSX
. As a result, both OSX
and macOS
can be used interchangeably.
Next, you need to change the availability of generateRecordInfo()
so it also works on macOS.
In the previous change, you combined iOS and OS X in a single attribute. However, that can only be done in attributes using that shorthand syntax; for any other @available
attribute, you need to add multiple attributes for different platforms.
Directly after the deprecation attribute, add the following:
@available(OSX, deprecated:10.11, message:"Use generateContactInfo()") |
This is the same thing as the line above it, but specifies for OS X instead of iOS.
Switch to the Persona-macOS target scheme, select My Mac as the build device, and build the project. There is one error in generateRecordInfo()
, at the following code block:
let person = ABPersonCreate().takeRetainedValue() let people = ABPersonCreatePeopleInSourceWithVCardRepresentation(person, data).takeRetainedValue() let record = NSArray(array: people)[0] |
The Contacts framework is a little different between iOS and macOS, which is why this error popped up. To fix this, you want to execute different code on iOS and macOS. This can be done using a preprocessor command.
Replace the previous code with the following:
#if os(iOS) let person = ABPersonCreate().takeRetainedValue() let people = ABPersonCreatePeopleInSourceWithVCardRepresentation(person, data).takeRetainedValue() let record = NSArray(array: people)[0] as ABRecord #elseif os(OSX) let person = ABPersonCreateWithVCardRepresentation(data).takeRetainedValue() as AnyObject guard let record = person as? ABRecord else { fatalError() } #else fatalError() #endif |
This makes the code work the same way on both platforms.
Linking Up the UI
Now that you finished updating PersonPopulator
, setting up ViewController
will be a breeze.
Open Persona-macOS’s ViewController.swift and add the following line to awakeFromNib()
:
getNewData(nil) |
Next, add the following to getNewData(_:)
:
let firstName: String, lastName: String, title: String, profileImage: NSImage if #available(OSX 10.11, *) { let (contact, imageData) = PersonPopulator.generateContactInfo() firstName = contact.givenName lastName = contact.familyName title = contact.jobTitle profileImage = NSImage(data: imageData)! } else { let (record, imageData) = PersonPopulator.generateRecordInfo() firstName = record.value(forProperty: kABFirstNameProperty) as! String lastName = record.value(forProperty: kABLastNameProperty) as! String title = record.value(forProperty: kABTitleProperty) as! String profileImage = NSImage(data: imageData)! } profileImageView.image = profileImage titleField.stringValue = title nameField.stringValue = "\(firstName) \(lastName)" |
Other than some small differences between iOS and macOS APIs, this code looks very familiar.
Now it’s time to test the macOS app. Change the target to Persona-macOS and select My Mac as the build device. Run the app to make sure it works properly.
Newton seems impressed! By making just those small changes to PersonPopulator
, you were able to easily port your iOS app to another platform.
More Info About @available
Availability attributes can be a little confusing to format and to use. This section should help clear up any questions you may have about them.
These attributes may be placed directly above any declaration in your code, other than a stored variable. This means that all of the following can be preceded by an attribute:
- Classes
- Structs
- Enums
- Enum cases
- Methods
- Functions
To indicate the first version of an operating system that a declaration is available, use the following code:
@available(iOS, introduced: 9.0) |
The shorthand, and preferred syntax, for marking the first version available is shown below:
@available(iOS 9.0, *) |
This shorthand syntax allows you to include multiple “introduced” attributes in a single attribute:
@available(iOS, introduced: 9.0) @available(OSX, introduced: 10.11) // is replaced by @available(iOS 9.0, OSX 10.11, *) |
Other attributes specify that a certain declaration no longer works:
@available(watchOS, unavailable) @available(watchOS, deprecated: 3.0) @available(watchOS, obsoleted: 3.0) |
These arguments act in similar ways. unavailable
signifies that the declaration is not available on any version of the specified platform, while deprecated
and obsoleted
mean that the declaration is only relevant on older platforms.
These arguments also let you provide a message
to show when the wrong declaration is used, as you used before with the following line:
@available(OSX, deprecated:10.11, message: "Use generateContactInfo()") |
You can also combine a renamed
argument with an unavailable
argument that helps Xcode provide autocomplete support when used incorrectly.
@available(iOS, unavailable, renamed: "NewName") |
Finally, the following is a list of the platforms you can specify availability for:
- iOS
- OSX
- tvOS
- watchOS
- iOSApplicationExtension
- OSXApplicationExtension
- tvOSApplicationExtension
- watchOSApplicationExtension
The platforms that end with ApplicationExtension are extensions like custom keyboards, Notification Center widgets, and document providers.
Note: The asterisk in the shorthand syntax tells the compiler that the declaration is available on the minimum deployment target on any other platform.
For example, @available(iOS 9.0, *)
states that the declaration is available on iOS 9.0 or greater, as well as on the deployment target of any other platform you support in the project.
On the other hand, @available(*, unavailable)
states that the declaration is unavailable on every platform supported in your project.
Where to Go From Here?
Here is the final project to compare with your own.
Availability Attributes make the task of supporting various platforms and versions in your applications extremely easy. Designed directly into the Swift language, they work with the compiler and with you to streamline the process of adding cross-platform and version compatibility to your project.
If you have any questions or comments about how I’ve used Availability Attributes in this tutorial, let me know in the comments below!
The post Availability Attributes in Swift appeared first on Ray Wenderlich.