Supporting Multiple iOS Versions and Devices

There are many different iOS versions and devices out there in the wild. Supporting more than just the latest is often necessary since not all users upgrade immediately. This tutorial shows you how to achieve that goal. By Pietro Rea.

Leave a rating/review
Save for later
Share
You are currently viewing page 2 of 6 of this article. Click here to view the first page.

Identifying Backwards Compatibility Issues

Backwards compatibility problems are so common it’s almost guaranteed you’ll hit them at some point in your iOS development career. Fortunately, there are several tools at your disposal that can help you quickly identify potential problems:

Older iOS devices

The best way to test backwards compatibility of your apps to run them on older iOS devices. It’s difficult to revert the iOS version on a device, so if at all possible don’t update the OS that shipped with your device. With this strategy, you’ll eventually form a collection of legacy iOS devices for testing and development.

Note: Ray asked his Twitter followers to submit photos of their iOS collections. Check out the “winners” at the end of this tutorial; the largest collection boasts over 15 iOS devices!

iOS simulator

If you can’t get your hands on a physical device, the next best thing is testing your app in the simulator running an earlier version of iOS. Xcode 4.6 supports iOS versions back to 5.0 which should be sufficient for most of your backwards compatibility tests. However, if you really need to run your app against a version earlier than 5.0, you can get there with a bit of hackery and tweaking.

Apple docs

Apple’s official documentation is incredibly useful when you need to know which version of iOS introduced a particular class or method. For example, look up UITableView and you’ll see at the top of the document that UITableView was introduced in iOS 2.0.

If you need to know when a particular property or method was introduced, you can find this in the “Availability” subheading, as shown in the screenshot below:

RWRageFaces 7

Header Files

Sometimes the best documentation of methods and classes is contained right in the source code.

Simply Command-click the symbol you want to inspect, and Xcode will take you to its header file, where you will find the information you need along with the parameters of the preprocessor macros NS_AVAILABLE_IOS() and NS_CLASS_AVAILABLE_IOS(). If a class or method declaration doesn’t use either of those availability macros, you can assume that they won’t cause any compatibility issues.

Note: ALT + click on a method will display a popover that contains these same details along with other information about the method.

API diffs

Apple publishes an exhaustive list of everything that was added or modified in each release of iOS. As an example, the iOS 6.0 API diff documents all the changes and additions associated with iOS 6.

API diffs won’t necessarily help you with in the nitty-gritty details of development, but they’re recommended reading to any developer who needs to be aware of issues involved with supporting older versions of iOS.

Deploymate

Wouldn’t it be great if Xcode compared your code against your deployment target and alerted you about any unsupported APIs? Until that day comes the Mac application Deploymate will do the job for you.

Deploymate is a static analyzer that scans your source code and warns you when it encounters unsupported or deprecated APIs. You’ll learn more about Deploymate in the project section of this tutorial, which includes a detailed demo of Deploymate in action.

MJGAvailability

Tutorial team member Matt Galloway, has released a simple header file that solves the problem of knowing when you’re using APIs that are not available in the deployment target. It solves the problem by tricking the compiler into thinking that such APIs are deprecated. Of course, they’re not, but the compiler thinks they are and warns as necessary.

Solving Backwards Compatibility Issues

Once you have identified the backwards compatibility issues in your app, the next step is to figure out how to fix them. Each new version of iOS introduces new frameworks, classes, methods, constants and enumeration values — and there’s a specific strategy to deal with each of these.

Unsupported frameworks

Linking against a framework that doesn’t exist in your deployment target will just cause the dynamic linker to crash your app. To solve this, you mark unsupported frameworks as “Optional” in your project settings.

To do this, select your project under the “Targets” section, and open up Build Phases -> Link Binary With Libraries. Next to each framework you can specify either Required or Optional. Selecting Optional will weak link the framework and solve your compatibility issue.

Weakly linking a library in Xcode

Note: You can read more about weak linking in Apple’s Framework Programming Guide.

Unsupported classes

Sometimes you want to use a class that exists in your base SDK, but not in your deployment target. To do this you need to check the availability of this class at runtime to avoid crashing your app. It crashes because this is what the Objetive-C runtime will do if you try to use a class that doesn’t exist. As of iOS 4.2, classes are weakly linked so you can use the +class method to perform the runtime check. For example:

if ([SLComposeViewController class]) {
    //Safe to use SLComposeViewController 
} else {
    //Fail gracefully
}

Unsupported methods

Similarly, if you’re using a method in your base SDK that doesn’t exist in your deployment target, you can avoid nasty crashes by using a little introspection.

The methods -respondsToSelector: and +instancesRespondToSelector: will both do the trick, as shown in the code examples below:

if ([self.image respondsToSelector:@selector(resizableImageWithCapInsets:resizingMode:)]) {
    //Safe to use this way of creating resizable images
} else {
    //Fail gracefully
}

The same goes for verifying the existence of class methods, except you call respondsToSelector: on the class itself, like so:

if ([UIView respondsToSelector:@selector(requiresConstraintBasedLayout)]) {
    //Safe to use this method
} else {
    //Fail gracefully
}

Note: If you want to check for the presence of a certain property on a class, then you can do so by testing that instances respond to the property getter or setter.

For example, to check if UILabel can take an attributed string via its attributedText property (new in iOS 6), perform introspection against the implicit setter method @selector(setAttributedText).

Unsupported constants/C functions

Sometimes a constant value is the missing piece of the deployment target; it usually takes the form of an extern NSString * or a C function. In this case, you can perform a runtime check against NULL to determine if it exists.

For example, the C function ABAddressBookCreateWithOptions(...) was introduced in iOS 6 but can still live safely in your iOS 5 app like so:

if (ABAddressBookCreateWithOptions != NULL) {
    //Safe to use
}
else {
    //Fail gracefully
}

The same approach applies to constants. For example, iOS 4.0 introduced multitasking support. If you wanted to check for the existence of UIApplicationWillEnterForegroundNotification, you would simply validate it as so:

if (&UIApplicationWillEnterForegroundNotification) {
    //Safe to assume multitasking support
}
else {
    //Fail gracefully
}

For further proof, take a look at UIApplication.h in Xcode. You’ll see that UIApplicationWillEnterForegroundNotification is simply an extern NSString * declared at the bottom of the document.

When your application is loaded into memory, these strings are initialized and stay resident in memory. The & operator gets the string’s memory address. If the memory address is not equal to nil, then the NSString is available, otherwise it’s not available and your code will have to work around that fact.

Note: The mechanism that makes this work is weak linking, which was described earlier. When a binary is loaded the dynamic linker replaces in the app binary any addresses of things (functions, constants, etc) in dynamically loaded libraries. If it’s weakly linked then if the symbol in the library is not found then the address is set to NULL.

Unsupported enumeration values

Checking for the existence of enumeration or bit mask values — the kind that you would find inside an NS_ENUM or NS_OPTIONS declaration — is incredibly difficult to check at runtime. Why?

Under the hood, an enumeration is just a method of giving names to numeric constants. An enum is replaced with an int when compiled, which always exists!

If you’re faced with the situation of needing to see if a certain enumeration exists, all you can do is either explicitly check the OS version (which isn’t recommended) or alternatively check against another API element that was introduced at the same time as the new enumeration value.

Note: Whichever way you do this, be sure to add adequate comments to any such code and consider wrapping the code in a dedicated compatibility helper.

Explicit iOS version checking

You should generally stay away from checking explicit iOS versions but there are specific situations where it’s unavoidable. For example, if you need to account for a bugfix in a previously available method, use the following line of code to return the OS version:

NSString *osVersion = [[UIDevice currentDevice] systemVersion];

You can use NSString’s compare:options: method, passing NSNumericSearch as the options to compare OS versions. This takes into account the fact that 6.1.1 is greater than 6.1.0. If you converted them to floats first and then used standard arithmetic things would go wrong because both would parse as the number 6.1.

Note: You can find out more about this topic in Apple’s SDK Compatibility Guide.

Contributors

Over 300 content creators. Join our team.