From: http://www.raywenderlich.com/42591/supporting-multiple-ios-versions-and-devices
When you’re writing an app for iOS, you likely test on the simulator and one or two devices. You might even test on more than one or two versions of iOS.
But doing so would cover only a handful of the different combinations of iOS version and device type. The amount of combinations add up quick!
In this tutorial you’ll see how to use various techniques that allow you to support different iOS versions and devices effectively. You’ll have your apps supporting multiple iOS versions and devices in no time.
In this tutorial, you’ll modify an existing iOS 6 app calledRWRageFaces to make it compatible for iOS 5 – without losing any of the iOS 6 functionality.
Along the way, you’ll cover various methods to detect and correct compatibility issues – and you’ll encounter a whole lot of crashes that give you some insight into the various APIs that we take for granted in iOS 6.
iOS Versions: An Overview
Each new version of iOS presents new opportunities to mobile developers — along with new challenges. Two years ago, iOS 5 introduced Storyboards, iCloud, Newsstand, and native Twitter integration. Last year, iOS 6 introduced Auto Layout, UICollectionView, PassKit, and native Facebook integration.
This year, Apple is promising a host of new features in iOS7, including Text to Speech, AirDrop for iOS, Sprite Kit, and other incredibly exciting innovative extensions to iOS.
However, new iOS versions are double-edged swords; along with the new features comes the inevitable issues with backwards compatibility. While you’re chomping at the bit to use the new features in your apps, some of the new classes and frameworks just aren’t available in older versions of iOS.
Just to add to the confusion, Apple mobile hardware has also evolved over the years. Back cameras, front cameras, gyroscopes, magnetometers, retina screens, taller iPhone screens and multiple CPU cores are all innovations that have been added over the years in different versions of the iPad and iPhone.
Fortunately, it’s not as hard as you might think to ensure your apps support the widest range of iOS versions and hardware devices possible. This tutorial presents several methods of supporting multiple flavors of software and hardware so that you can offer the best user experience — no matter what platform your users are on!
Note: To keep an eye on iOS feature support by version, log in and check out Apple’s iOS checklist. This checklist was introduced at WWDC 2013, and is expected to change significantly when iOS7 is released!
Getting Started
Download the sample project RWRageFaces, extract it to a convenient location, and open it up in Xcode.
Note: The download is quite large because there are quite a few images in the app bundle!
Build and run the app; the first thing you’ll see is a large collection view of rage faces grouped by section, as shown below :
Rotate your device; you’ll note that the view rotates as expected, as shown below:
When you tap a rage face, the app presents a modal view controller with a larger version of the selected rage face. Below the image is the category name in red followed by the image name in black (e.g. “Happy: oh stop it you.png”).
There’s also a “Share” button on the top right that lets you share the rage face via e-mail, Facebook, or Twitter — or you can simply copy the image to Pasteboard, as shown below:
But wait, there’s more! The modal view controller contains a UIPageViewController
with all the rage faces in the category your selected rage face belongs to. Swipe left and right to browse through the rage faces, as so:
But before going any further with your app, there’s a bit of thought that needs to go into why you need to support multiple iOS versions…and when to draw the line.
Why Bother Supporting Multiple iOS Versions?
The biggest benefit to supporting multiple iOS and hardware versions is to increase marketability of your app.
Despite the convenience of over-the-air (OTA) OS updates, some folks will just never update their OS. Unless you specifically back-support old versions, these late adopters (or non-adopters) won’t be able to download your application from the App Store and give it the love it deserves.
So therefore you should support as many OS versions as you can, right?
Not so fast. Backwards compatibility takes time and effort to get right. In many cases, you’ll need to re-engineer APIs just so your app’s features can run on older versions of iOS — time that might be better spent polishing other features in your app.
To make an educated decision regarding which iOS versions to support, you must understand Apple’s adoption rates and release cycles. Apple doesn’t make official announcements about iOS adoption statistics on a regular basis — you’ll need to keep your ear to the ground to get the most recent stats.
For example, in January 2013 Apple announced that 300 million mobile devices were already running iOS 6, accounting for approximately 60% of all iOS devices. Five months later, Apple announced that 93% of all iOS devices were running iOS 6.0 or better.
Based on those statistics, it looks like the case for supporting iOS5 gets weaker by the month. Michael Jurewitz, a former Apple frameworks evangelist, believes that any time spent supporting old iOS and hardware versions is a waste of time.
Jurewitz makes a good argument to limit the time you spend thinking about backwards compatibility. However, sometimes building a great app means going against the rules.
Deployment Target vs. Base SDK
Before we get started making RWRageFaces support iOS 5, there’s a few bits of theory to go over first. When discussing backwards compatibility, two terms come up frequently — deployment target and base SDK.
Base SDK refers to the newest version of iOS that is capable of running your app. To check which base SDK you are building your app against, simply open your project file in Xcode and check the setting underBuild Settings -> Architecture, as shown in the screenshot below:
In Xcode 4.6 you have two options for Base SDK; “iOS 6.1” and “Latest iOS”. “Latest iOS” should be selected by default and will reference the most recent version of iOS; this means you don’t have to worry about updating your base SDK when Apple releases a new version of iOS.
Deployment Target refers to the oldest version of iOS that is capable of running your project. To change your deployment target, open up your project file in Xcode and check the setting under Build Settings -> Deployment, as shown in the screenshot below:
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:
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 macrosNS_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 willweak link the framework and solve your compatibility issue.
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 anextern 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 thatUIApplicationWillEnterForegroundNotification
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 theNSString
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 anNS_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.
Supporting iOS 5 in RWRageFaces
Okay – time to put this theory into action and add iOS 5 back support capability in RWRageFaces!
Click on your project file, and under “Info” change the iOS Deployment Target to iOS 5.0, as shown below:
Select the iPad 5.0 or iPad 5.1 Simulator on the top left of your screen, as shown below:
Build and run your app, and…crash!
The app immediately crashes at launch with the following error message:
dyld: Library not loaded: /System/Library/Frameworks/Social.framework/Social |
dyld is the name of the dynamic linker in iOS and Mac OSX. The app crashes because it uses the Social framework for sharing on Facebook and Twitter — but the Social framework was introduced in iOS 6, and dyld can’t find it in the iOS 5 simulator.
To fix this, first select your app’s target in Xcode. Select the Build Phases tab, and under Link With Binary Libraries, make sure Social.framework
is set to Optional instead of Required, as shown below:
Build and run your app, and…another crash? What’s going on here?
The error message displayed is different this time; maybe it offers up some clues:
NSInvalidUnarchiveOperationException, reason: ‘Could not instantiate class named UICollectionView’ |
Ah, that’s the issue. The app is trying to use UICollectionView
to create the grid of rage faces; the problem is that collection views were not added until iOS 6 and the simulator only knows about objects in iOS 5.
Note that the exception is related to unarchiving; this is a clue that iOS is trying to unarchive aUICollectionView
from either a NIB file or a storyboard and failing.
Open MainStoryboard.storyboard in Xcode. You can see that the initial view controller contains aUICollectionView
, as shown below:
To get around this problem, you’re going to use an open source library authored by Peter Steinberger called PSTCollectionView
. Download the library from Github.
Note: The link above downloads the 1.1.0 release of PSTCollectionView. Feel free to download a more up-to-date version if one exists.
Unzip the library and drag the entire PSTCollectionView directory to the frameworks directory in Xcode, as shown below:
As per the instructions found on Github, you also require the QuartzCore framework to work with this library. Click on your target and add QuartzCore on the Build Phases tab, under the Link With Binary Libraries section and make it Required, as so:
PSTCollectionView
is a faithful implementation of UICollectionView
that you can use in deploys all the way back to iOS 4.3.
Helpfully, PSTCollectionView
also has a mode whereby you can make it detect if the device is running iOS 6 and if so use UICollectionView
, otherwise use PSTCollectionView
‘s own classes. That’s very neat.
Let’s use this shiny toy!
Add the following import to RWGridViewController.m:
#import "PSTCollectionView.h"
|
Next, go through RWGridViewController.m and find and replace all occurrences of UICollectionViewwith PSUICollectionView. That includes replacing all occurrences of UICollectionView at the start of words as well, meaning you will make the following replacements:
- UICollectionView becomes PSUICollectionView
- UICollectionViewDataSource becomes PSUICollectionViewDataSource
- UICollectionViewDelegateFlowLayout becomes PSUICollectionViewDelegateFlowLayout
- UICollectionViewCell becomes PSUICollectionViewCell
- UICollectionViewLayout becomes PSUICollectionViewLayout
Once you’ve prefixed all appearances of UICollectionView*
with “PS”, build and run your project.
Once again, Xcode crashes in a spectacular manner:
This time, the error reads:
*** Terminating app due to uncaught exception 'NSInvalidUnarchiveOperationException', reason: 'Could not instantiate class named NSLayoutConstraint’ |
Right…NSLayoutConstraint
is used by Auto Layout which wasn’t introduced until iOS 6.
Working around Auto Layout in iOS 5
To fix this issue, open the main storyboard and click on the File Inspector. Click on the Use Autolayoutcheckbox to disable it, as shown below:
Build and run your app and…and…it starts normally! If you experienced a crash due to an exception related to UICollectionView
, go back and double-check that you’ve prefixed everything with “PS”.
Rotate your device, or if you’re on the simulator, press Command+Left/Right arrow.
Hm. The app doesn’t rotate like it used to. That’s because iOS 5 and iOS 6 call different rotation methods.
Add the following code to RWGridViewController.m below viewDidLoad
:
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation { return YES; } |
This method is an old iOS 5 method that tells UI Kit what orientations are supported for this view controller. It wasn’t necessary when the app supported just iOS 6, but it’s now necessary to support iOS 5 as well.
Build and run your app and try to rotate the device or simulator again.. The app rotates properly now — but the navigation bar isn’t in the right spot, as shown in the image below:
By turning off auto layout, the navigation bar uses its autosizing mask; that is, using springs and struts. Looks like you’re not done yet!
Head back to the main storyboard, and select the navigation bar in the Grid View Controller Scene. In the Size Inspector on the right, click on the top strut to turn it on. Do the same for the nav bar inRWDetailViewController
Next, turn on flexible width and height for the collection view in RWGridViewController
. This causes the collection view to expand to the maximum height and width when rotated.
Once again, build, run and rotate; the rotation works as you would expect.
Now pick any rage face and click on it. Crash again!
You will see something like this in the console:
2013-08-02 21:52:46.127 RWRageFaces[1126:c07] *** Terminating app due to uncaught exception 'NSInvalidUnarchiveOperationException', reason: 'Could not instantiate class named UIStoryboardEmbedSegueTemplate' *** First throw call stack: (0x14a0022 0x1195cd6 0x1448a48 0x14489b9 0x6194a3 0x61967b 0x619383 0x3bdf99 0x51a135 0x619c6e 0x619383 0x519cad 0x619c6e 0x61967b 0x619383 0x519105 0x722eef 0x723477 0x3c05ab 0x33b7 0xfb94 0xf73d 0xef01 0x14a1e99 0x3e5c49 0x3e5cb6 0x14a1e99 0x3e5c49 0x3e5cb6 0x5bca1a 0x147499e 0x140b640 0x13d74c6 0x13d6d84 0x13d6c9b 0x22ab7d8 0x22ab88a 0x2f8626 0x242d 0x2355) terminate called throwing an exception |
This means an exception was thrown. You’re going to need to turn on an exception breakpoint to see what the problem actually is. To do this, go to the breakpoint navigator – the 6th tab in the left panel in Xcode and click the + button in the bottom left. Then click Add Exception Breakpoint…. Then click Donein the popup that appears.
Build & run the app again and do the same thing as before – click on any rage face. Now you’ll see that it’s throwing an exception in the following line:
[self performSegueWithIdentifier:@"toDetailViewController" sender:indexPath]; |
It seems like there’s a problem with -performSegueWithIdentifier:
. Navigate back to the main storyboard and check out RWDetailViewController
: it contains a UIPageViewController
via an embed segue.
Embed segues make it easy to manage the containment of view controllers using Interface Builder, but they weren’t introduced until iOS 6. Your iOS 5 target won’t support them.
Working Around Embed Segues in iOS 5
Open MainStoryboard.storyboard.
Select the container view embedded inside Detail View Controller Scene (it says “Container” in the middle) and delete it. Next, select the dangling UIPageViewController
and delete it. This automatically gets rid of the embed segue in Interface Builder.
While you’re here, you should also pin the navigation bar in the detail view controller to the top, just like you did in the grid view controller. This will ensure the navigation bar sticks to the top of the view.
Next, open up RWDetailViewController.m and delete prepareForSegue:
, which was used to set up the child view controller using the embed segue. You’ll have to do view controller containment the old way!
Still in RWDetailViewController.m, replace viewDidLoad
with the following:
- (void)viewDidLoad { [super viewDidLoad]; RWRageFaceViewController *rageFaceViewController = [self.storyboard instantiateViewControllerWithIdentifier:@"RWRageFaceViewController"]; rageFaceViewController.index = self.index; rageFaceViewController.imageName = self.imageNames[self.index]; rageFaceViewController.categoryName = self.categoryName; //Initialize UIPageViewController programatically self.pageViewController = [[UIPageViewController alloc] initWithTransitionStyle:UIPageViewControllerTransitionStyleScroll navigationOrientation:UIPageViewControllerNavigationOrientationHorizontal options:nil]; CGFloat navBarHeight = self.navigationBar.frame.size.height; CGRect detailViewControllerFrame = CGRectMake(0, navBarHeight, self.view.frame.size.width, self.view.frame.size.height - navBarHeight); self.pageViewController.view.frame = detailViewControllerFrame; self.pageViewController.view.autoresizingMask = (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight); self.pageViewController.delegate = self; self.pageViewController.dataSource = self; //Add UIPageViewController as a child view controller [self addChildViewController:self.pageViewController]; [self.view addSubview:self.pageViewController.view]; [self.pageViewController didMoveToParentViewController:self]; [self.pageViewController setViewControllers:@[rageFaceViewController] direction:UIPageViewControllerNavigationDirectionForward animated:NO completion:nil]; } |
In the code above, you initialize the UIPageViewController
in code instead of initializing it automatically via the storyboard. You then set its frame, delegate and data-source and then add it as the child view controller of RWDetailViewController
.
Build and run your app, and tap on any rage face. Xcode still crashes — but the crash originates from a different location: RWRageFaceViewController.m:
[attributedTitle addAttribute:NSForegroundColorAttributeName value:[UIColor redColor] range:categoryRange]; |
In the original iOS 6 app the label below the rage face image displays the rage face’s category name in red and the name of the image in black, using an NSAttributedString
to do the job.
You can probably guess that NSAttributedString
wasn’t introduced until…iOS 6!
Working Around NSAttributedString in iOS 5
The easiest way to fix this is to populate the UILabel
using an NSAttributedString
in iOS 6, and a regularNSString
in iOS 5.
Open RWRageFaceViewController.m and replace viewDidLoad
with the following:
- (void)viewDidLoad { [super viewDidLoad]; self.imageView.image = [UIImage imageNamed:self.imageName]; NSString *titleString = [NSString stringWithFormat:@"%@: %@", self.categoryName, self.imageName]; /* Use NSAttributedString with iOS 6 + */ if ([self.imageLabel respondsToSelector:@selector(setAttributedText:)]) { NSMutableAttributedString *attributedTitle = [[NSMutableAttributedString alloc] initWithString:titleString]; NSRange categoryRange = [titleString rangeOfString:[NSString stringWithFormat:@"%@: ", self.categoryName]]; NSRange titleRange = [titleString rangeOfString:[NSString stringWithFormat:@"%@", self.imageName]]; [attributedTitle addAttribute:NSForegroundColorAttributeName value:[UIColor redColor] range:categoryRange]; [attributedTitle addAttribute:NSForegroundColorAttributeName value:[UIColor blackColor] range:titleRange]; self.imageLabel.attributedText = attributedTitle; } /* Simple UILabel with iOS 5 */ else { self.imageLabel.text = titleString; } } |
This has changed the method to first check for the existence of UILabel
’s attributedText
property, just like you saw earlier in the theory part of the tutorial. If the test passes, you know it’s safe to use an attributed string.
Build & run the project one more time, and tap on any rage face. Success — the app doesn’t crash! Now, simply swipe left or right to change screens and…wait, what happens?
Even though you initialized the UIPageViewController using transition styleUIPageViewControllerTransitionStyleScroll
, the app is behaving as if the page view controller had been initialized using the page curl transition style. What’s going on?
This behavior occurs because UIPageViewControllerTransitionStyleScroll
is an enum value that was introduced in iOS 6. In iOS 5, the page view controller doesn’t know what to do with the scrolling transition style so it defaults to the page curl transition style.
As discussed before, there’s no way to check against the existence of an enum value at runtime because it evaluates to an integer. It would be like asking the question, “Does the integer 1 exist?”.
Working Around Page Transitions in iOS 5
To get around this problem you’ll have to check the availability of the constantUIPageViewControllerOptionInterPageSpacingKey
, which was also introduced in iOS 6.
In RWDetailViewController.m, change viewDidLoad
to match the following implementation:
- (void)viewDidLoad { [super viewDidLoad]; RWRageFaceViewController *rageFaceViewController = [self.storyboard instantiateViewControllerWithIdentifier:@"RWRageFaceViewController"]; rageFaceViewController.index = self.index; rageFaceViewController.imageName = self.imageNames[self.index]; rageFaceViewController.categoryName = self.categoryName; CGFloat navBarHeight = self.navigationBar.frame.size.height; CGRect detailViewControllerFrame = CGRectMake(0, navBarHeight, self.view.frame.size.width, self.view.frame.size.height - navBarHeight); /* Use UIPageViewController in iOS 6+ */ //1 if (&UIPageViewControllerOptionInterPageSpacingKey) { //2 self.pageViewController = [[UIPageViewController alloc] initWithTransitionStyle:UIPageViewControllerTransitionStyleScroll navigationOrientation:UIPageViewControllerNavigationOrientationHorizontal options:@{UIPageViewControllerOptionInterPageSpacingKey: @(35)}]; self.pageViewController.delegate = self; self.pageViewController.dataSource = self; self.pageViewController.view.frame = detailViewControllerFrame; self.pageViewController.view.autoresizingMask = (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight); [self.view addSubview:self.pageViewController.view]; [self.pageViewController setViewControllers:@[rageFaceViewController] direction:UIPageViewControllerNavigationDirectionForward animated:NO completion:nil]; } //3 else { [self addChildViewController:rageFaceViewController]; rageFaceViewController.view.autoresizingMask = (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight); rageFaceViewController.view.frame = detailViewControllerFrame; [self.view addSubview:rageFaceViewController.view]; [rageFaceViewController didMoveToParentViewController:self]; } } |
Here’s what’s going on in the above code, step by step:
- Check for the string constant
UIPageViewControllerOptionInterPageSpacingKey
using the & operator. If the memory address exists, that the constant exists as well and you must be running on iOS 6. - Since you’ve discovered that
UIPageViewControllerOptionInterPageSpacingKey
exists, you might as well use it to add a 35 point margin between the rage face view controllers. - If
UIPageViewControllerOptionInterPageSpacingKey
is not available, it means your app is running on an iOS 5.X device or below. Since the scrolling transition style is not available, forgo the page view controller entirely and simply show one rage face at a time.
Build and run again and tap on any rage face. Notice that you can’t scroll left or right anymore; that’s the intended behavior in iOS 5 so you can move on.
Note: Depending on your needs, it’s perfectly fine to drop a feature in an older version of iOS and focus your efforts elsewhere. If you really need to support the scrolling transition style in iOS 5, you’re faced with finding an open source solution to fit your needs or implement your ownUIPageViewController
. Both options can be very time consuming, so make sure it’s worth the effort before going down that road!
While RWDetailViewController
is up, tap on the Share button on the top right corner to reveal an activity sheet inside a popover.
Tapping on either of the Twitter or Facebook options crashes the app in iOS 5. For example, tapping on Facebook crashes in the following place:
if ([SLComposeViewController isAvailableForServiceType:SLServiceTypeFacebook]) { |
Sharing via Twitter or Facebook uses SLComposeViewController
which — you guessed it — didn’t exist in iOS 5.
The easiest way to fix something that’s broken is to get rid of it altogether. Draconian? Yes. Effective? Totally. :]
Working around SLComposeViewController in iOS 5
Scroll down to the share button’s IBAction
method -shareButtonTapped:
and rework the implementation as so:
- (IBAction)shareButtonTapped:(id)sender { //Email, Facebook, Twitter, Clipboard self.actionSheet = [[UIActionSheet alloc] initWithTitle:@"Share" delegate:self cancelButtonTitle:nil destructiveButtonTitle:nil otherButtonTitles:nil]; [self.actionSheet addButtonWithTitle:@"E-mail"]; if ([SLComposeViewController class]) { [self.actionSheet addButtonWithTitle:@"Facebook"]; } [self.actionSheet addButtonWithTitle:@"Twitter"]; [self.actionSheet addButtonWithTitle:@"Clipboard"]; [self.actionSheet showFromBarButtonItem:self.shareButton animated:YES]; } |
In this new implementation, the SLComposeViewController
is tested to see if it exists. If so, then the Facebook share button is shown, otherwise it is not. That’s because SLComposeViewController
is required for Facebook sharing.
But what about Twitter? That’s a bit easier since even though Twitter native support is now part of the Social framework, the same code did exist in iOS 5, but just under the specific Twitter framework.
The functionality will remain the same, but your app will now use TWTweetComposeViewController
instead of SLComposeViewController
for both iOS 5 and iOS 6.
To add the Twitter framework, go to Build Phases -> Link Binary With Libraries for your target. AddTwitter.framework
as a “Required” library. Make sure not to remove the Social framework because it is still going to be used for native Facebook sharing in iOS 6.
Go back to RWDetailViewController.m and add the following import statement at the beginning of the file to support the framework you just added:
#import <Twitter/Twitter.h>
|
Open RWDetailViewController.m, find actionSheet:clickedButtonAtIndex:
. Then look for the following bit of code:
RWRageFaceViewController *rageFaceViewController = self.pageViewController.viewControllers[0]; NSString *imageName = rageFaceViewController.imageName; UIImage *image = [UIImage imageNamed:imageName]; |
…and replace it with the following code:
NSString *imageName = self.imageNames[self.index]; if (self.pageViewController) { RWRageFaceViewController *rageFaceViewController = self.pageViewController.viewControllers[0]; imageName = rageFaceViewController.imageName; } UIImage *image = [UIImage imageNamed:imageName]; |
Essentially, this ensures that you share the correct image in each OS version. If you are on iOS 5, there will only be one image per RWDetailViewController
which you access throughself.imageNames[self.index]
.
However, if the app is running on iOS 6 there will be a UIPageViewController
and the image that you share is determined by whatever view controller UIPageViewController
is displaying at the moment.
If you look at the rest of this method you will see that it uses the index of the button tapped to determine the behaviour. This logic no longer works because Facebook may or may not be present, so you’ll use the name of the button instead.
Add the following code to the start of actionSheet:clickedButtonAtIndex:
:
NSString *buttonTitle = [actionSheet buttonTitleAtIndex:buttonIndex]; |
And then replace the if-statement conditions in the same method with the following:
if ([buttonTitle isEqualToString:@"E-mail"]) { /* E-mail*/ // ... same content as before ... } else if ([buttonTitle isEqualToString:@"Facebook"]) { /* Facebook */ // ... same content as before ... } else if ([buttonTitle isEqualToString:@"Twitter"]) { /* Twitter */ // ... same content as before ... } else if ([buttonTitle isEqualToString:@"Clipboard"]) { /* Copy to Clipboard */ // ... same content as before ... } |
This is changing from using the button index to determining which button was pressed by its title. This is required now because the button index no longer maps to exactly which button was pressed because on iOS 5 there are a different number of buttons to that on iOS 6.
In actionSheet:clickedButtonAtIndex:
, scroll down to the else-if
statement that handles Twitter sharing and modify the implementation as follows:
else if ([buttonTitle isEqualToString:@"Twitter"]) { /* Twitter */ if ([SLComposeViewController class] && [SLComposeViewController isAvailableForServiceType:SLServiceTypeTwitter]) { SLComposeViewController* twitterVC = [SLComposeViewController composeViewControllerForServiceType:SLServiceTypeTwitter]; [twitterVC setInitialText:@"Check out this Rage Face at raywenderlich.com via @RWRageFaces"]; [twitterVC addURL:[NSURL URLWithString:@"http://www.raywenderlich.com"]]; [twitterVC addImage:image]; [self presentViewController:twitterVC animated:YES completion:nil]; } else if ([TWTweetComposeViewController class] && [TWTweetComposeViewController canSendTweet]) { TWTweetComposeViewController *twitterVC = [[TWTweetComposeViewController alloc] init]; [twitterVC setInitialText:@"Check out this Rage Face at raywenderlich.com via @RWRageFaces"]; [twitterVC addURL:[NSURL URLWithString:@"http://www.raywenderlich.com"]]; [twitterVC addImage:image]; [self presentViewController:twitterVC animated:YES completion:nil]; } else { UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"No Twitter Accounts" message:@"Please add a Twitter account by going to Settings > Twitter" delegate:nil cancelButtonTitle:@"Cancel" otherButtonTitles:nil]; [alertView show]; } } |
The above code ensures that TWTweetComposeViewController
is used on iOS 5 and the newer,SLComposeViewController
is used on iOS 6 where it is present. Even thoughTWTweetComposeViewController
is due to be deprecated, you need to use it on iOS 5 as that’s the only way to achieve Twitter sharing natively.
Build & run the app and go to share a rage face. W00t! No longer can you click Facebook and all the existing share options still work, including Twitter.
That’s the end of the changes required to ensure RWRageFaces works on iOS 5; it doesn’t support all the bells and whistles that iOS 6 offers, but it neatly handles the difference between the two apps by disabling or sidestepping features as necessary.
Deploymate Sanity Check
You’ve fixed all the compatibility issues in your app — or have you? An old rule of testing says that you can’t fix the bugs that don’t yet know they exist. :] Before shipping, it’s a good idea to give your app a run through Deploymate.
Deploymate is a static analyzer for OS X created by Ivan Vasic (@ivanvasic). It scans your source code and reports on any unsupported APIs based on your deployment target; it’s useful as a quick sanity check before submitting to the App Store. You can download Deploymate here.
Note: The demo is functional but it doesn’t report all API problems, just random ones — so don’t rely on the demo version for production releases!
When you first open Deploymate, you’ll see the following Launch screen:
Select Open existing Xcode project and navigate to the RWRageFaces.xcodeproj file from the RWRageFaces project. Open the project file and a rather familiar-looking UI will be presented, as shown below:
Verify that your “Deployment OS” is set to the correct target (iOS 5 in this case) and click Analyze.
Deploymate takes anywhere from a few seconds to a few minutes to scan through your entire project; the total time really depends on the number of files to analyze. When it’s finished the analysis, the results screen will look something like the following:
Whoa — hang on a second. Why are there 26 unavailable API calls if you just fixed all the backwards compatibility issues in RWRageFaces?
Remember that Deploymate is a static analyzer, so it’s just doing a compile-time check for unsupported symbols. As of this writing, it cannot follow your conditional logic to see what is and isn’t going to be executed at runtime.
Click on any of the issues on the left side, and Deploymate will show you the code that raised the issue, as demonstrated in the screenshot below:
In this particular case Deploymate is complaining about NSForegroundColorAttributeName
, which was introduced in iOS 6.
Notice that NSForegroundColorAttributeName
is wrapped in an if statement that checks if UILabel
can accept attributed strings, so it’s won’t be called on iOS 5 devices.
Take the time to step through every issue and make sure you’ve handled each case in a way that’s not going to cause a crash or other undefined behavior at runtime.
One last thing to keep in mind with Deploymate in its current form doesn’t scan NIB or storyboards files; so it would not have prompted you to turn off Auto Layout or removing the embed segue as you did earlier to correct the crashing behavior.
Supporting Multiple Devices
This tutorial has only covered backwards compatibility as it relates to software; however, making sure your apps can run on a wide range of hardware is just as important.
The obvious hardware differences between the iPhone and the iPad won’t be covered in this tutorial. However, check out the tutorial on porting an iPhone app to iPad and the one on updating your apps to the 4-inch iPhone 5 display since they are related to the topic of supporting multiple devices.
The idea behind maintaining backwards compatibility in hardware is simple: iOS devices can have (or lack) a variety of different hardware components. Just as you performed a runtime check to verify if a particular API was available, you have to perform a runtime check to see if a hardware component is available before you attempt to use it.
But what if the device doesn’t have the hardware you need for your app? If a particular hardware component is absolutely crucial to your application, it’s best if you limit the distribution of your application to devices that have such components. Can you imagine installing a flashlight app on a device with no camera flash?
You can specify the hardware features required by your app by changing theUIRequiredDeviceCapabilities
key in your project’s Info.plist. Click on your target and navigate toInfo/Required device capabilities, as shown below:
The UIRequiredDeviceCapabilities
key lets you include or exclude device capabilities such front camera, back camera, camera flash, GPS, magnetometer, gyroscope, Bluetooth and more. Refer to Apple’s Information Property List Key Reference Guide and for a full list.
Note: Right-click on the project’s Info.plist and choose Open As > Code to edit theUIRequiredDeviceCapabilities
key. The array
value will suffice if you just want to list required capabilities. However, if you want to state both required capabilities and capabilities that devices must not have, then you can convert it to a dictionary. Then you use true
or false
values for each device capability to indicate which capabilities are required and which must not be present.
When a hardware component is not critical to your application, you may choose to hide or disable it if the component is not present. For example, you can use the following two methods to check if a device has a back or a front camera:
if ([UIImagePickerController isCameraDeviceAvailable:UIImagePickerControllerCameraDeviceRear]) { //safe to use back camera } if ([UIImagePickerController isCameraDeviceAvailable:UIImagePickerControllerCameraDeviceFront]) { //safe to use front camera } |
isCameraDeviceAvailable:
is available as of iOS 4.0. You have to do a few additional checks if your deployment target is earlier than iOS 4.0, which should be unlikely at this point!
UIImagePickerController
is also helpful for checking if your camera has flash capabilities and video recording capabilities.
To check if your device supports a microphone, use the following AVFoundation API:
AVAudioSession *session = [AVAudioSession sharedInstance]; if (session.inputIsAvailable) { //safe to use microphone } |
inputIsAvailable
is deprecated as of iOS 6. Use inputAvailable
instead if your deployment target is iOS 6 or above.
Additionally, you can use Core Motion’s CMMotionManager
to detect if your device has a gyroscope, a magnetometer or an accelerometer.
CMMotionManager *motionManager = [[CMMotionManager alloc] init]; if (motionManager.gyroAvailable) { //safe to use gryoscope } if (motionManager.magnetometerAvailable) { //safe to use magnetometer } if (motionManager.accelerometerAvailable) { //safe to use accelerometer } |
gyroAvailable
and accelerometerAvailable
are available as of iOS 4. magnetometerAvailable
, on the other hand, is available as of iOS 5. To check for a magnetometer prior to iOS 5, use CLLocationManager
‘sheadingAvailable
API, which is available in the Core Location framework.
There are a few more hardware components you could check for that haven’t been covered here. The main takeaway, however, is that you should never assume that a particular hardware component is present on a device, as that assumption might not hold true for older devices — or for newer ones!
Device Collection Winners
As promised, here are the coolest iOS collections from users that responded to Ray’s tweet.
Longest straight line (@libovness):
Most beautiful Collection (@blakespot):
Biggest collection (@_DavidSmith):
Having your own collection of test devices is not only cool to look at, but is really helpful during development and testing.
Where To Go From Here?
Congratulations! You’ve succesfully navigated the art and science of backward compatibility on iOS. Hereis the completed RWRageFaces project — fully compatible on iOS 5 and iOS 6.
Even though the tutorial dealt mainly with iOS 5 and iOS 6, the techniques presented here such as runtime checks, introspection and weak linking will be useful for many more iOS versions to come.
Always keep in mind that not everyone is a power user. If you pay close attention to backwards compatibility, your users will definitely thank you for making sure your applications are well supported on whichever system they are running.