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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Supporting Multiple iOS Versions and Devices
50 mins
- iOS Versions: An Overview
- Getting Started
- Why Bother Supporting Multiple iOS Versions?
- Deployment Target vs. Base SDK
- Identifying Backwards Compatibility Issues
- Solving Backwards Compatibility Issues
- Supporting iOS 5 in RWRageFaces
- Working around Auto Layout in iOS 5
- Working Around Embed Segues in iOS 5
- Working Around NSAttributedString in iOS 5
- Working Around Page Transitions in iOS 5
- Working around SLComposeViewController in iOS 5
- Deploymate Sanity Check
- Supporting Multiple Devices
- Device Collection Winners
- Where To Go From Here?
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 regular NSString
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 style UIPageViewControllerTransitionStyleScroll
, 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 constant UIPageViewControllerOptionInterPageSpacingKey
, 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 own UIPageViewController
. Both options can be very time consuming, so make sure it’s worth the effort before going down that road!
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 own UIPageViewController
. 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. :]