程序支持不同iOS的版本和设备

注:本文翻译之Supporting Multiple iOS Versions and Devices

第一次翻译这么长的E文,深深的感觉到自己的E文还是有很大的上升空间的,翻译处女作,如果有什么翻译不当或者错误的地方,请多多指教,谢谢你的光临!

正文

当你编写一个iOS APP 的时候, 你很可能在模拟器或者多个设备上面测试,甚至你会在iOS的不同版本之前测试。

但是这样做只能够覆盖少数不同组合的iOS版本和设备类型,组合的类型增加很迅速

在本篇的教程中你将会见识怎样运用各种各样的技术来有效的让你支持不同的iOS版本和设备,你将会很快的让你的apps支持不同的iOS版本和设备。

在本篇教程中,你将会修改一个已经存在的名为RWRageFaces的iOS 6应用,让它兼容iOS 5版本,与此同时,并不会丢失iOS 6的相关功能。

一路上,你将会涉及到各种各样的方法来发现和修改兼容性的问题,同时也会遭遇到许多的程序在iOS6中崩溃问题。对于许多的APIs,我们理所当然会给你一些见解。

iOS版本:概述

每一个新版本的iOS发布,对于移动开发者而言,都是一个新的机遇,同时也是一个挑战。两年前,iOS 5 中引进 Storyboards, iCloud, Newsstand 和 集成的原生态的Twitter。去年,iOS 6 中引入了 Auto Layout, UICollectionView, PassKit 和 集成的原生态的Facebook。

今年,苹果在iOS 7 同样加入了很多新的特性,包括 Text to Speech, AirDrop for iOS, Sprite Kit 和 一些非常令人兴奋的扩展。

然而,新版本的iOS确实一把双刃剑;同时新的特性必然会造成程序向后兼容的问题。而你也只能够咬咬牙使用一小部分新的特性,因为很多的新类和框架并不能够在旧版本中使用。

苹果的移动设备已经发展多年了,后置摄像头,前置摄像头,陀螺仪,磁力仪,视网膜屏幕,拉长的屏幕和多核CPU都已经添加到不同版本的iPhone和iPad中去了.

很庆幸的是,确保你的程序能够支持大范围的版本和设备并没有你想象的那么难,本篇教程将会展示一些不同的软件和硬件让你的用户在不同的平台中都能够享受到最好的用户体验。

Note:留意一下不同iOS对应的特性功能,查看苹果的iOS清单,这篇清单介绍了WWDC 2013,并且大众期待的iOS 7 将会释放!

准备开始

下载RWRageFaces工程文件,下载到一个便利的地点,在Xcode中打开它。

下载可能有点儿慢因为在程序中包含了不少的图片资源!

编译并且运行这个程序;首先你将会看到一个巨大的已经分段的愤怒表情图像集合视图,如下图所示:

转动你的设备,你将会发现这些视图如所预期的那样旋转,如下图所示:

当你点击一个愤怒的表情,应用程序将会展现一个模态的视图来显示一个大尺寸的选中愤怒表情图像,在图像的下方,红色字体的是种类名称,黑色字体的是图片的名称.

在视图的右上角还有一个”Share”按钮,让你能够通过e-mail, Facebook和Twitter来分享表情,或者你可以把它负载到剪切板上面,如下图所示:

别急,还有更精彩的!这个模态的视图控制器包含了一个 UIPageViewController,你可以向左或者向右混动来查看同一类型的表情,如下图所示:

在更加深入了解你的app之前,有一些小小的思考有关为什么需要支持不同的iOS版本…并且到什么地步是个底线

为什么支持不同的iOS版本如此的烦恼?

支持不同iOS版本和设备最大的收益就是可以使你的应用程序增加市场覆盖率。

尽管拥有方便无线的系统更新,仍然有部分人不去更新他们的系统,除非你明确的支持这些旧版本,这些晚采用者(或者不采用者)将无法下载到他们爱的并且值得的应用程序。

那么你是不是要尽你的最大努力去支持尽可能多的系统呢?

并没有那么的容易,向后兼容需要花费时间和努力来让它变得正确,在许多的情况下,你将会需要重新设计APIs来让你的app的特性可以在旧版本中运行,将会花费大量的时间来做磨合。

让一个受过教育的决定在哪个iOS版本支持,您必须理解苹果的采用率和发布周期。苹果不让官方公告关于iOS采用定期统计——你需要保持获取最新数据。

例如,在2013年1月宣布,3亿年苹果移动设备已经运行iOS 6,占大约60%的iOS设备。五个月后,苹果公司宣布,93%的iOS设备上运行iOS 6.0或更好。

基于这些统计数据,它看起来像案例支持iOS5按月也会变得更弱。Michael Jurewitz,前苹果框架传教士花, 认为任何人支持老iOS和硬件版本是浪费时间

Jurewitz使一个好论点,以限制你花时间思考的向后兼容性。然而,有时建筑一个伟大的应用程序意味着违背规则。

Deployment Target Vs. Base SDK

在准备使 RWRageFaces 支持iOS 5 之前,首先需要重温一下理论知识,当我们决定要向后兼容,出现两个复杂的关系–deployment targetbase SDK.

Base SDK 指的是最新版本的iOS,能够运行你的程序。检查您正在构建的基础SDK应用程序对,只需要你打开项目文件在Xcode,检查下面的Build Settings- >Architecture,如下面屏幕截图所示:



在Xcode 4.6有两个选项用来基地SDK;“iOS 6.1”和“Latest iOS”。“Latest iOS”应该默认选中,将参考最新版本的iOS;这意味着你不必担心更新你的base SDK当苹果发布一个新版本的iOS。

Deployment Target 指的是老版本的iOS,能够运行您的项目。改变你的部署目标,打开项目文件在Xcode,检查下面的Build Settings -> Deployment,如下面屏幕截图所示:



## 识别向后兼容的问题

在你的iOS开发生涯中,几乎可以肯定你将会碰到向后兼容性的问题。幸运的是,有一些工具可以帮助你快速的处理和识别潜在的问题:

### 较旧的iOS设备

最好的方法测试向后兼容就是让你的应用程序在旧版本的iOS设备上面运行.在一个设备上面,你很难恢复之前的系统版本,所以你要尽可能的跳过升级系统,你将会很多的不同版本的设备供测试和开发。

>Note:Ray问他的Twitter追随者提交他们的iOS的照片收藏,在教程末尾是挑选出来的胜利者,最大的iOS设备收集超过了15部!

### iOS模拟器

如果你手头并没有物理设备,其次是测试你的应用程序在模拟器运行iOS的早期版本。Xcode 4.6支持iOS版本回到5.0,应该足够你的大部分向后兼容性测试。然而,如果你真的需要运行你的应用程序早于5.0版本,你可以参考别人的做法

### 苹果官方文档(Apple docs)

苹果的官方文档非常有用,当你需要知道哪个版本的iOS引入一个特定的类或方法。例如,抬头,你会看到UITableView的文档,UITableView 在 iOS 2.0中引入。

如果你需要知道当一个特定属性或方法的介绍,你可以发现这在“Availability”副标题,如下面屏幕截图所示:



### 头文件(Header Files)

有时最好的方法和类的文档包含正确的源代码。

简单的command-click组合键表示你想检查,Xcode将带你去它的头文件,在那里你可以找到你需要的信息随着参数的预处理器宏NS_AVAILABLE_IOS()NS_CLASS_AVAILABLE_IOS()。如果一个类或方法声明不使用任何这些可用性宏,你可以假定他们不会造成任何兼容性问题。

>Note:ALT + click 将会在xcode中出现一个包含基本解释信息的弹出框

### API diffs

苹果发布一个详尽的清单的东西添加或修改在每次发布iOS的时候。作为一个例子,iOS 6.0 API diff文件相关的所有更改和添加。

API差别不一定会帮你在细节的发展,但是他们推荐阅读任何开发人员需要了解的问题参与支持旧版本的iOS。

### Deploymate

如果你的Xcode代码部署目标和提醒你关于任何不受支持的api那就更好了?那一天到来之前的Mac应用程序Deploymate将帮您完成这项工作。

Deploymate是一个静态分析器,扫描你的源代码,并警告您当遇到不受支持的api或废弃。这节教程你会了解更多关于在项目中的Deploymate,其中包括一个详细的演示Deploymate的行为。

### MJGAvailability

教程团队成员 Matt Galloway,发布了一个简单的头文件,在Deployment Target下,来知道你使用的APIs是不是可用的,它解决了这个问题通过诱骗编译器认为这样的api是弃用。当然,他们不是,但编译器认为他们和警告是必要的。

## 向后兼容问题的解决

一旦你确定了在你的应用程序存在向后兼容性问题,下一步是找出并修复它们。每个新版本的iOS都会引入新框架、类、方法、常量、枚举值,这是一个特定的策略来处理每一种。

### 未支持框架(Unsupported frameworks)

在Deployment Target版本中连接了未被支持的框架会让程序在运行的时候崩溃,为了解决这个,你需要在工程中设置这些未支持的框架为”Optional”.

为了达到这个目的,选择你的工程中的”Targets”区段,打开Build Phase->Link Binary With Libraries.每一个框架你都可以指定Required或者Optional, 选中 Optional 将作为一个弱连接,这样就可以解决兼容问题。



>Note:你可以了解更多有关弱连接,访问 Apple’s Framework Programming Guide

### 不支持的类(Unsupported classes)

有的时候你需要添加一些base SDK中的类,但是这些类却在Deployment target中不存在。你需要检查运行总的类是否可用来避免程序的崩溃。崩溃的原因就是在Objective-C运行的时候运用了一个不存在的类。如iOS 4.2中,类都是弱连接的,所以你可以运用方法 +class 来执行运行检查,例如:

1
2
3
4
5
if ([SLComposeViewController class]) {
//Safe to use SLComposeViewController
} else {
//Fail gracefully
}


### 不支持的方法(Unsupported methods)

同样的,如果你使用了base SDK中的方法,但是在Deployment target中却不存在,为了避免这些,你可以用一些内省。

-respondsToSelector:+instancesRespondToSelector:就是做同样的事情,例如:

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


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


>Note:如果你要检查类中是否存在某些属性,你可以测试实例的有关属性的getter和setter方法。

>例如:为了查看UILabel是否有attributedText属性(iOS 6引入的),执行显示的setter方法@selector(setAttributedText).

### 未支持的常量/C 函数

有些时候常量在Deployment Target中会有所缺失,它们通常的形式格式为extern NSString 或者 C 函数。这种情形下,您可以执行一个运行时检查NULL来确定它的存在。

例如,这个C函数ABAddressBookCreateWithOptions(…)在 iOS 6中引入,但是任然可以在你的 iOS 5中存在,像这样:

1
2
3
4
5
6
if (ABAddressBookCreateWithOptions != NULL) {
//Safe to use
}
else {
//Fail gracefully
}


同样的方法也适用于常量,例如,iOS 4.0中引入了多任务的支持,如果你想检查UIApplicationWillEnterForegroundNotification是否存在,你需要编写简单的检测如下所示:

1
2
3
4
5
6
if (&UIApplicationWillEnterForegroundNotification) {
//Safe to assume multitasking support
}
else {
//Fail gracefully
}


为了更进一步的证明,查看Xcode中的UIApplication.h文件,你将会看到UIApplicationWillEnterForegroundNotification就是一个简单的常量,extern NSString 声明在文档的底部。

当你的程序加载到内存中的时候,那些常量也都初始化并且贮存在内容中,&操作时获取常量的内存地址,如果内存地址不为nil,则这个常量是可用的,否则它使不可用的。

>Note:这个机制使工作变成弱连接,之前讨论过的,当一个二进制加载动态链接器取代了在应用程序二进制任何地址的事情(函数、常量等)在动态加载的库。如果是弱链接然后如果符号在库中没有被发现然后地址设置为NULL。

### 未支持的枚举值(Unsupported enumeration values)

检查声明在NS_ENUMNS_OPTIONS中的枚举值或者位掩码值,并且在运行时检查十分的困难,为什么?

深层次的,一个枚举值就是仅仅一个放回数字类型的方法,当编译的时候就回自动的替换为一个整型值,并且一直存在在系统中

如果你需要面对查看一个枚举值是否真实的存在,你要做的就是明确的检查系统的版本(当然了这个是不推荐的)或者检查另外新的API元素同时引入了新的枚举值。

>Note:无论你怎么做,一定要添加足够的评论任何这样的代码,并考虑包装代码在一个专用的兼容性助手。

### 明确iOS的版本检查

你一般要远离检查明确的iOS版本,但是有些特定情况下面,这是不可避免的。例如:如果你需要占用以前可以用的补丁修正方法,你需要使用下面的代码来返回系统的版本:

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


你可以用使用NSString的compare:options:方法,通过NSNumericSearch作为选项来比较系统版本。但是在实际的6.1.1大于6.1.0版本中,如果首先转换成浮点值,就是导致程序出错,并且它们转换后都返回6.1

>Note:你可以查看有关这个主题更加详细的介绍, Apple’s SDK Compatibility Guide.

## 让RWRageFaces支持iOS 5

好了,我们现在把理论知识运用到实际中,在RWRageFaces中添加支持 iOS 5 的能力!

点击你项目中的”PROJECT”,在”Info”选项卡中改变Deployment Target为 iOS 5, 如下图所示:



在左边的模拟器设备中选择iPad 5.0 或者 iPad 5.1,如下图所示:



编译并且运行你的应用程序,然后…就没有然后了!



应用程序在启动的时候就立马崩溃了,并且打印出下面的错误信息:

1
dyld: Library not loaded: /System/Library/Frameworks/Social.framework/Social


dyld是iOS和Mac OSX中的一个动态的链接库。程序崩溃的原因就是程序用使用了分享到Facebook和Twitter的Social framework,但是这个库是在 iOS 6 中引入的,所以dyld在 iOS 5 的模拟器中不能够找到具体的实现。

为了解决,首先在Xcode中选择你的应用程序的Target, 选择Build Phases选项卡,在Link With Binary Libraries下面,使Social.framework变成Optional,如下图所示:



再一次编译运行,然后…另外的一个崩溃又发生了。



这一次的错误信息好像有些不同,也许它可以提供一些线索:

1
NSInvalidUnarchiveOperationException, reason: ‘Could not instantiate class named UICollectionView


这就是问题所在了,在应用程序我试图使用UICollectionView来创建表格试图;问题就是直到 iOS 6 中才添加了collection试图。

注意这个错误关系到unarchiving,线索就是iOS试图从一个NIB文件或者storyboard中解档一个UICollectionView失败了。

在Xcode中,打开MainStoryboard.storyboard,你可以看到在初始化的view controller中包含了一个UICollectionView,如下图所示:



为了避免这个问题,你将使用Peter Steinberger写的一个开源的库,名叫PSTCollectionView.从Github上面下载此库.

解压刚刚下载的库,然后拖动全部的PSTCollectionView文件夹到Xcode中的frameworks中,如下图所示:



通过Github上面的介绍可知,你需要添加 QuartzCore framework到工程中.如下图所示:



PSTCollectionView是一个很可靠的实现了UICollectionView的功能,你可以在 iOS 4.3版本之后部署它。

更有用的就是,PSTCollectionView可以确定自己用那个模型来适应运行的环境,如果在 iOS 6中,就会自动切换到UICollectionView,否则就会用PSTCollectionView自己定义的类,非常的优雅。

RWGridViewController.m中导入:

1
#import "PSTCollectionView.h"


接下来,浏览整个RWGridViewController.m,把UICollectionView替换成PSUICollectionView。这样下面的一些还需要进行替换:

UICollectionView变成PSUICollectionView UICollectionViewDataSource变成PSUICollectionViewDataSource
UICollectionViewDelegateFlowLayout变成PSUICollectionViewDelegateFlowLayout UICollectionViewCell变成PSUICollectionViewCell
UICollectionViewLayout变成*PSUICollectionViewLayout

一旦你都修改好之后,编译运行你的应用程序,
但是Xcode再一次惊人的崩溃了:

这一次,错误信息如下:

1
*** Terminating app due to uncaught exception 'NSInvalidUnarchiveOperationException', reason: 'Could not instantiate class named NSLayoutConstraint’

准备的。。。NSLayoutConstraint是来用自动布局的,直到 iOS 6中才引入的。

处理iOS 5中的自动布局问题

为了解决这个问题,你需要打开 main storyboard并且点击 File Inspector,取消Use Autolayout,如下图所示:

再次编译和运行,这一次就能够很正常的运行了,如果还会出现崩溃,并且错误信息有关UICollectionView,请参照上面把它们修改到PSTCollectionView中的一些类

旋转你的设备,如果你在是模拟器上面工作,按Command+Left/Right arrow.

但是,应用程序并没有像所想象的那样旋转,因为 iOS 5 和 iOS 6 调用了不同的有关旋转的方法。

RWGridViewController.m的viewDidLoad方法下面添加如下代码:

1
2
3
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation {
return YES;
}

这个方法是在 iOS 5中的一个旧方法,目的是告诉UIKit什么样的方向对于view controller是可用的,在iOS 6中这个方法不是必须的,但是在 iOS 5中是必须要用的。

编译&运行你的程序,试着再次选装设备或者模拟器,应用程序正常选择,但是navigation bar却不在正确的位置,效果如下图所示:

通过关闭自动布局功能,导航条设置了自己的自动布局方式,也就是说,使用springs和struts.看起来你做得还不够!

我们再次回到main storyboard,并且在Grid View Controller Scene选择navigation bar.在Inspector的右边,把top struct打开,在RWDetailViewController也做同样的事情。

接下来,在RWGridViewController中的collection view设置宽度和高度自适应大小。通过这个可以当屏幕旋转的时候可以让collection view自动适应屏幕的大小。

再一次,编译&运行&选择;这一次程序运行的很完美了!

如果你选择一个头像表情,程序再次崩溃了!

你将会看到一些错误信息出现在控制台:

1
2
3
4
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

这个表示这是一个异常,你将要打开一个异常的断点看看到底问题出现在哪一行,为了达到这个目的,你需要在断点导航栏(breakpoint navigator)-在第六个选项卡左下角点击+,然后点击Add Exception Breakpoint,然后在弹出框中点击Done

编译&运行,再次像上面的操作流程,当你点击头像表情的时候,你将会看到具体的异常在哪一行:

1
[self performSegueWithIdentifier:@"toDetailViewController" sender:indexPath];

看起来是由于-performSegueWithIdentifier:这个方法导致的,在main storyboard中选择RWDetailViewController,它通过embed segue的方式包含了一个UIPageViewController.

在Interface Builder 中 Embed segues 能够很方面的管理视图控制器间的传递。但是这些东西只能够在 iOS 6 中运用,而在 iOS 5中却不支持.

处理 iOS 5 中的 Embed Segues

打开 MainStoryboard.storyboard.

选择容器视图嵌入Detail View Controller Scene(它说“容器”在中间),并删除它。接下来,选择UIPageViewController和删除它。在界面构建器中就自动地摆脱了embed segue。

接下来,打开RWDetailViewController.m,删除prepareForSegue:,你不得不把这个转为旧的方式。

同样是在RWDetailViewController.m用下面的代码替换viewDidLoad:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
- (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];

}

在上面的代码中,你需要自己初始化UIPageViewController代替通过storyboard的自动创建,然后你需要设置它的frame,delegate和datasource,然后把它作为孩子节点添加到RWDetailViewController中。

编译&运行应用程序,然后点击头像表情,Xcode仍然崩溃,但是此时崩溃的地点发生了变化:RWRageFaceViewController.m:

1
2
3
[attributedTitle addAttribute:NSForegroundColorAttributeName
value:[UIColor redColor]
range:categoryRange];

在原始的iOS 6应用程序 label 下面的图像显示图像表情的类别名称为红色和图像的名称为黑色,使用一个NSAttributedString来做这项工作。

你大概可以猜到,直到 iOS 6 中才推出NSAttributedString!

处理 iOS5中的NSAttributedString

最简单的办法来解决这个是填充使用NSAttributedString UILabel在iOS 6,并具有规则的NSString在iOS 5。

打开RWRageFaceViewController.m,然后替换viewDidLoad中的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
- (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;
}
}

改变方法第一件事件就是检查UILabel中的attributedText属性是否存在,就像上面理论部分教程的内容一样,如果检查通过了,我们就知道使用attributed字符串是安全的。

再一次编译&运行,点击任意的头像表情,程序不出现崩溃的现象了,现在可以进行简单的滑动手势,向左或者向右滑动,但是界面好像和我们预期的有些出入:

即使你初始化了 UIPageViewController 使用了转换的类型 UIPageViewControllerTransitionStyleScroll,但是不知道为什么会出现这种现象?

出现这样的情况就是因为UIPageViewControllerTransitionStyleScroll是一个枚举值,在 i0S 6 中引入。在 iOS 5 中,page view controller并不知道这个值具体代表什么,所以就选择了默认的转换类型。

正如前面所讨论的一样,没有办法核对枚举值的存在在运行时因为它计算结果为整数。这就像问的问题,“是整数1存在吗?”。

处理 iOS 5 中的 Page Transitions

为了解决这个问题,你需要检查这个UIPageViewControllerOptionInterPageSpacingKey常量是不是可用,而它也是定义在 iOS 6中。

RWDetailViewController.m中,改变viewDidLoad的实现方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
- (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];
}
}

一步一步解释上面的意图:

1.检查常量 UIPageViewControllerOptionInterPageSpacingKey 使用 & 操作符,如果内存地址存在,就说明常量存在,你必须运行在 i0S 6上。

2.既然你已经发现 UIPageViewControllerOptionInterPageSpacingKey存在,你可以添加35点的相差在头像表情视图控制器上面。

3.如果 UIPageViewControllerOptionInterPageSpacingKey 不可用,这说明应用程序运行的版本在 iOS5.0或者以下。既然切换的类型不可用,那就放弃使用page view controller,一次只显示一个头像表情。

再次编译&运行,点击头像表情,注意,这次你不能够左右滑动,因为程序运行在 iOS 5上面,这样你就可以继续操作了。

Note:根据您的需求,就完全可以放弃一个功能在一个旧版本的iOS和专注你的工作在其他地方。如果你真的需要支持滚动过渡风格在iOS 5,你面临着找到一个开源的解决方案,以满足您的需要或实现自己的UIPageViewController。两个选项是非常耗时间的,所以确保值得努力去之前,沿着这条路!

RWDetailViewController 出现在屏幕的最前端,点击右上角的 Share 按钮,将会出现一个弹出提示框。

选择Facebook 或者 Twitter都会在 iOS 5的情况下面崩溃,例如,点击 Facebook 崩溃地点如下:

1
if ([SLComposeViewController isAvailableForServiceType:SLServiceTypeFacebook]) {

分享功能使用的是 SLComposeViewController,你可以猜得出来,在 iOS 5 中是不存在的。

最简单的方法处理就是丢弃这些功能点,但是这样真的有效吗?

处理 iOS 5 中的 SLComposeViewController

向下滚动到分享按钮的 IBAction 方法 -shareButtonTapped:,重新实现这个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- (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];
}

在这个新的实现,SLComposeViewController测试,看看是否存在。如果是这样,那么Facebook分享按钮显示,否则就不是。这是因为SLComposeViewController要求Facebook分享。

然后就是Twitter了,这是一个比较容易因为即使Twitter原生支持现在的Social framework,相同的代码确实存在,但只是在iOS 5在特定的Twitter框架。

这个功能将保持不变,但是你的应用程序将使用TWTweetComposeViewController代替SLComposeViewController,可以同时兼顾 iOS 5 和 iOS 6.

为了添加 Twitter 框架,移步设置target Build Phases -> Link Binary With Libraries添加Twitter.framework 作为 “Required” 库,确保不要删除 Social framework 因为它使为了facebook使用的。

回到 RWDetailViewController.m,添加:

1
#import <Twitter/Twitter.h>

打开 RWDetailViewController.m,找到actionSheet:clickedButtonAtIndex:方法,然后找到如下代码:

1
2
3
RWRageFaceViewController *rageFaceViewController = self.pageViewController.viewControllers[0];
NSString *imageName = rageFaceViewController.imageName;
UIImage *image = [UIImage imageNamed:imageName];

然后替换成下面的代码:

1
2
3
4
5
6
7
8
NSString *imageName = self.imageNames[self.index];

if (self.pageViewController) {
RWRageFaceViewController *rageFaceViewController = self.pageViewController.viewControllers[0];
imageName = rageFaceViewController.imageName;
}

UIImage *image = [UIImage imageNamed:imageName];

从本质上讲,这可以确保你分享正确的图像兼容每个操作系统版本。如果你在iOS 5,那里只有一个图像每RWDetailViewController您访问通过self.imageNames[self.index]。

然而,如果应用程序是运行在iOS 6将会有一个UIPageViewController和形象,你分享是由任何视图控制器是目前UIPageViewController显示。

如果你看看剩下的这个方法你会发现它使用的索引键来确定行为了。这个逻辑不再工作,因为Facebook可能或可能不存在,那么您将使用按钮名称代替。

actionSheet:clickedButtonAtIndex:的开头,添加:

1
NSString *buttonTitle = [actionSheet buttonTitleAtIndex:buttonIndex];

然后替换判断的逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
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 ...
}

这是改变使用按钮index确定哪个按钮被点击。这是必需的,因为这个按钮indx不再映射到哪些按钮被按下,因为在iOS 5和 iOS 6中有不同数量的按钮。

actionSheet:clickedButtonAtIndex:方法中,向下滚动到 else-if语句,处理Twitter,修改其实现方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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];
}
}

上面的代码确保TWTweetComposeViewController用在iOS 5和更新,SLComposeViewController是用在它存在的iOS 6。尽管TWTweetComposeViewController将被弃用,你需要使用它在iOS 5作为唯一方法是实现Twitter分享本地。

编译&运行程序,这一次一些就变得非常的完美了。

Deploymate Sanity Check

你认为自己能够修改所有的兼容性问题,但是一些旧有的规则会告诉你,你不可能解决所有的bugs,因为压根你就不知道还有什么问题存在,在程序打包之前,用工具(Deploymate)来检测你的程序是一个很好的主意。

Deploymate 是lvan Vasic(@ivanvasic)专门为 OS X 写的静态分析工具,它可以根据你的Deployment target找出你代码中有哪些不被支持的APIs,在你上传应用程序到App Store之前,它能够很快的完美的检测出存在的问题,你可以在这里下载

Note:演示版并不能够报告所有的API问题,只是其中的一小部分,所以你不应该依赖于演示版本来进行工作,应该选择收费版本的强大功能!

当你第一次打开Deploymate,你将会看到如下图所示:

选择 Open existing Xcode project, 然后选择RWRageFaces工程中的 RWRageFaces.xcodeproj,然后就出现一个很熟悉的页面,如下图所示:

检查你的”Deployment OS” 是不是选择正确的(当前是iOS 5),然后点击 Analyze

Deploymate将会花一点儿时间来浏览你整个工程,花销的时间长短取决于你工程文件的多少。在它完成分析之后,你就可以看到如下信息:

哇,等一下,为什么出现了26个不可用的APIs调用。

谨记,Deploymate是一个静态(static)分析工具,所以它按照你的程序已经检查,并不去思考你的代码时候在运行时做动态的变换。

在左边的导航栏中随便选择一个问题,然后 Deploymate 就会跳转到具体的哪里代码引起的问题的地点:

在实际的情况中,Deploymate指出NSForegroundColorAttributeName是在 iOS 6 中引入的。

注意,NSForegroundColorAttributeName是包装在一个判断条件里面的,检查UILabel是不是能够运用这个属性的,所以这些内容在 iOS 5的设备中并不运行。

花一点时间,把所有的问题过一下,确保每一个检测出来的问题都不会引起程序的崩溃.

最后一点不足的就是Deploymate并不会去浏览你的NIB或者storyboards文件,所以你最好关闭Auto Layout或者删除embed segue。

支持多设备

这篇教程只做了向后兼容,但是,要确保程序能够运行在不同的设备上面才是至关重要的。

很明显,除了iPhone和iPad以外的设备将不会在本教程中涉及。然而,查看移植iPhone应用程序到iPad上面或者更新应用程序来适应iPhone 5的4英寸的屏幕显示,显然这是和我们要讨论的支持多设备相关的主题。

保持向后兼容的硬件很简单:iOS设备没有太多的不同的硬件组件。就像你执行运行时检查是否一个特定的API是可用的,你必须执行运行时检查是否一个硬件组件可以在你尝试使用它。

但是,如果设备没有硬件你需要为你的应用程序?如果一个特定的硬件组件绝对是至关重要的,你的应用程序,它是最好的如果你限制你的应用程序的分配设备,有这样的组件。你能想象一个手电筒软件安装在一个设备没有照相机闪光灯吗?

通过改变你工程中的Info.plist,你可改变 UIRequiredDeviceCapabilities 的值来指定应用程序所需要的硬件特性。点击工程的target,然后在 Info/Required device capabilities,如下图所示:

UIRequiredDeviceCapabilities这个值可以让你设置设备的功能特点,例如前置摄像头(front camera),后置摄像头(back camera), 相机闪光灯(camera flash), GPS, 磁力仪(magnetometer), 陀螺仪(gyroscope),蓝牙(Bluetooth)或者更多。可以移步到苹果官方文档查看更加详细的信息。

如果你的应用程序不是很复杂,可以选择程序进行判断.例如:你可以用下面的两个方法来检车前置和后置摄像头是否可用:

1
2
3
4
5
6
7
if ([UIImagePickerController isCameraDeviceAvailable:UIImagePickerControllerCameraDeviceRear]) {
//safe to use back camera
}

if ([UIImagePickerController isCameraDeviceAvailable:UIImagePickerControllerCameraDeviceFront]) {
//safe to use front camera
}

isCameraDeviceAvailable:在 iOS 4中是可用的。

UIImagePickerController:也是一个有用的方法来检查你的摄像头是否有闪光灯、录像等功能。

如果要检查你的设备是否支持麦克风,你可以用下面的 AVFoundation API:

1
2
3
4
AVAudioSession *session = [AVAudioSession sharedInstance];
if (session.inputIsAvailable) {
//safe to use microphone
}

inputIsAvailable在 iOS 6中已经废弃你可以使用inputAvailable来代替。

附加的,你可以使用Core Motion中的 CMMotionManager来检查你的设备是否有陀螺仪、磁力仪和加速器。

1
2
3
4
5
6
7
8
9
10
11
12
13
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
}

gyroAvailableaccelerometerAvailable在 iOS 4中就可用了,而magnetometerAvailable只有在 iOS 5中才可用,为了使磁力仪能够在 iOS 5之前的系统中使用,可以使用CLLocationManager中的headingAvailableAPI,这个API在 Core Location framework中存在。

还有一些其他的硬件需要检查这里没有涉及到,唯一的宗旨就是不要工作在某一个特定的设备上,不然就不能够完美的运行在一个更新的设备或者旧的设备。

收集设备的赢家

最长的观光带(@libovness):

最漂亮的集合(@blakespot):

最齐全的集合(@DavidSmith):

文章目录
  1. 1. 正文
  2. 2. iOS版本:概述
  3. 3. 准备开始
  4. 4. 为什么支持不同的iOS版本如此的烦恼?
  5. 5. Deployment Target Vs. Base SDK
    1. 5.1. 处理iOS 5中的自动布局问题
    2. 5.2. 处理 iOS 5 中的 Embed Segues
    3. 5.3. 处理 iOS5中的NSAttributedString
    4. 5.4. 处理 iOS 5 中的 Page Transitions
    5. 5.5. 处理 iOS 5 中的 SLComposeViewController
  6. 6. Deploymate Sanity Check
  7. 7. 支持多设备
  8. 8. 收集设备的赢家
,