Quantcast
Channel: Kodeco | High quality programming tutorials: iOS, Android, Swift, Kotlin, Unity, and more
Viewing all articles
Browse latest Browse all 4387

Availability Attributes in Swift

$
0
0

Availability-featureWith 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:

Set the iOS 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:

Contacts was not introduced until iOS 9

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:

The availability switch causes iOS 9.0 to be printed

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:

E=MC²

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.

Download the iOS 8.4 simulator

When the simulator is finished downloading, select the iPhone 5 iOS 8.4 Simulator and click Run.

Select the iPhone 5 simulator

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.

New macOS target

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.

Set 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.

Drag in the new files

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:

Add vCards to the macOS target

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.

Sir Isaac Newton is impressed.

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.


Viewing all articles
Browse latest Browse all 4387

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>