同时兼容 Xcode 7 和 Xcode 8
做为 ios 开发者,你肯定会对 ios 10 中的新特性感到无比兴奋,并迫不及待地想要在应用中进行实践。虽然你想马上就动手以便第一时间就能“上船“。但 ios 10 正式上线却是几个月以后的事情,而且在那之前,你还需要保持每几周就发布一次的频率。这个情况听起来是不是跟你现在的处境很像呢?
当然,目前你还不能用 xcode 8 来编译需要发布的应用——因为它无法通过 app store 的验证。所以你需要把项目拆分成两个分支,稳定分支和 ios 10 开发分支……
而不可避免地是,这烂透了。如果只是在分支上做一点某个特性的开发还是可以的。但是如果是持续好几个月来维护这个庞大的分支呢?不仅它的整个代码库都发生了变化,而且主分支也一直在演进,这时候你就会碰到一些不可描述的合并之痛了。我说的是,你有尝试过处理 .xcodeproj 文件的合并冲突么?
这篇文章的目的就是告诉你如何彻底避免使用分支。对于大部分应用而言,只用一个工程文件就同时支持 ios 9(xcode 7)和 ios 10(xcode 8)是完全可能的。而且即使你不得不使用分支,这些小技巧也可以帮助你减少两个分支之间的差异,从而更舒服的对它们进行同步。
你要用的是 swift 2.3
我先说明一点:
我们都为 swift 3 而兴奋。它很棒,但是如果你正在读这篇文章,请别用它(或者暂时别)。虽然它很好,但是它在代码层面上存在很大的不兼容,比一年前 swift 2 的不兼容还要严重得多。而且一旦应用存在对第三方 swift 库的依赖,就得等这些库都升级到 swift 3,它才可以跟着升级。
而好消息是,同时也是史无前例的事情,xcode 8 支持两个版本的 swift:2.3 和 3.0。
为了防止你错过了某些通知,xcode 7 中的 swift 2.3 和 swift 2.3 基本是一致的,除了少数的 api 调整(之后会详细介绍)。
所以!为了保持兼容性,我们还是用 swift 2.3 来进行开发。
xcode 的设置
说这么多你应该已经很明白了。现在我来教你如何设置你的 xcode 项目,让它可以在这两个版本 上运行。
swift version
首先,在 xcode 7 中打开你的项目。然后打开项目的设置页,选中 build settings 选项,然后点击 “+“来增加一个 user-defined 设置项:喎? f/ware/vc/"="" target="_blank" class="keylink">vcd4ncjxwcmugy2xhc3m9"brush:java;"> “swift_version” = “2.3”
这个选项是 xcode 8 新增的,所以当它告诉 xcode 8 使用 swift 2.3 时,xcode 7(实际上它并没有 swift 2.3)会完全忽略这个设置并继续使用 swift 2.2 来进行构建。
framework provisioning
framework provisioning 的工作方式在 xcode 8 上稍有不同——如果是模拟器,它们会按原样继续编译,而对于真机会构建失败。
修复这个问题的方式是,遍历 build settings 中所有的 framework targets 并增加如下的选项,就像 swift_version:
“provisioning_profile_specifier” = “abcdefghij/“
你需要把“abcdefghij“替换成你的团队id(你可以在 apple developer portal 中找到它),然后保留最后的斜杠。
这实际上就是告诉 xcode 8“嘿,我是来自这个团队的,你注意下 codesign,好吗?“,然后 xcode 7 仍然会忽略这个设置,这样就万事大吉了。
interface builder
遍历所有的 .xib 和 .storyboard 文件,打开右侧边栏,选中第一个选项(file inspector),然后找到“opens in“设置项。
大部分情况下它显示的内容是 “default (7.0)“,把它修改为“xcode 7.0“。这可以保证即使你是在 xcode 8 中新建的这个文件,它也只能做一些可以向后兼容 xcode 7 的变动。
再次提醒一定要注意在 xcode 8 中对 xib 所做的改动。因为它会添加一些 xcode 版本相关的数据(不能确定的是应用上传到 app store 之后这些数据是否会被移除掉),而且某些时候它还会尝试把文件回滚到只支持 xcode 8 的格式(这是个 bug)。所以我们要尽可能避免在 xcode 8 中创建 interface 文件,如果实在没办法,那么再每次提交代码的时候都要仔细 review 代码,然后只提交你需要的那几行。
sdk version
确保所有的项目和构建目标的 “base sdk“设置项都被设置为 “latest ios“。(大部分情况下默认设置就是这样的,但是还是要再次确认下。)这样一来,xcode 7 就会针对 ios 9 来编译,同时同样的项目在 xcode 8 中就可以获得 ios 10 的新特性。
cocoapods settings
如果你用了 cocoapods, 你同样也需要更新 pods 项目的设置,确保其 swift 和 provisioning 的设置是正确的。
同时你也可以通过在 podfile 文件中添加如下 post-install 代码的方式来代替手动设置:
post_install do |installer| installer.pods_project.build_configurations.each do |config| # configure pod targets for xcode 8 compatibility config.build_settings['swift_version'] = '2.3' config.build_settings['provisioning_profile_specifier'] = 'abcdefghij/' config.build_settings['always_embed_swift_standard_libraries'] = 'no' end end
同样,记得把 abcdefghij 替换成你的团队 id。然后运行 pod install 来重新生成 pods 项目。
(如果发现这个 pod 不兼容 swift 2.3,那么你需要为 xcode 8 单独拉一个不同的分支, 这是由 igor palaguta 提供的)
在 xcode 8 中打开
好了,现在就可以在 xcode 8 中打开这个项目了。第一次打开的时候你会被大量的请求所轰炸
xcode 会提醒你更新到最新版本的 swift。忽略。
xcode 还会建议更新项目的设置为 “推荐设置“,同样忽略。
记住,我们已经对项目做了设置,让它可以在两个版本下都可以编译通过。所以现在我们要做的是尽量少做改动,从而保证同时兼容。更重要的是,因为我们发布到 app store 的文件是同一个,所以我们不希望 .xcodeproj 文件中包含任何 xcode 8 相关的数据。
处理 swift 2.3 的差异
就像我之前说过的,swift 2.3 和 swift 2.2 是相同的语言。然而,ios 10 sdk 的 frameworks 已经更新了一些 swift 的注释。我说的不是(那只是 swift 3.0 的事情)——但是,swift 2.3 的一些命名,类型和 api 的可选性还是稍微有些变化。
条件编译
考虑到你可能会忽略这一点, swift 2.2 就了编译预处理宏。用法很简单:
#if swift(>=2.3) // this compiles on xcode 8 / swift 2.3 / ios 10 #else // this compiles on xcode 7 / swift 2.2 / ios 9 #endif
太棒了!一个文件,不需要分支就同时兼容了 xcode 的两个版本
有两个需要注意的事项:
#if swift(<2.3) 这种写法是不存在的,只有 >=。如果要表达相反的意思,你可以写 #if !swift(>=2.3)。(如果需要的话你还可以使用 #else 和 #elseif)。 和 c 的预处理不同,#if 和 #else 必须是有效的 swift 代码,例如,你不能只改变方法签名而不改变方法体。(对于这点后面会有相应的处理方案)可选性的变化
swift 2.3 中很多签名都把不必要的可选性都去掉了,而有些(比如很多 nsurl 的属性)也变成 了可选值。
你当然也可以用条件编译来处理这个问题,比如:
#if swift(>=2.3) let specifier = url.resourcespecifier ?? "" #else let specifier = url.resourcespecifier #endif
但是下面的方法可能会小有帮助:
func optionalize(x: t?) -> t? { return x }
我知道这有点难理解。也许你看过结果之后就会容易得多了:
let specifier = optionalize(url.resourcespecifier) ?? "" // works on both versions!
这样就发挥了可选值的封装优势,从而避免在调用的时候写恶心的条件编译代码了。optionalize() 方法做的事情就是把任何传进去的值转换成可选值,除非传入的已经是可选值的情况,它就把参数直接返回。这样一来,不管 url.resourcespecifier 是(xcode 8)或者不是(xcode 7)可选值,“optionalized“版本永远是一样的。
(更深入地说:在 swift 里面, foo 可以被理解为 foo? 的子类,因为你可以在不丢失信息的情况下把任何一个 foo 类型的值封装成可选值。编译器一旦知道这点,它就允许传入一个非可选值到一个可选值参数中-把 foo 封装成 foo?。)
用别名来拯救方法签名的变化
swift 2.3 中,一些方法(特别是在 macos 的 sdk 中)修改了参数类型。
比如,之前 nswindow 的构造方法是这样的:
init(contentrect: nsrect, stylemask: int, backing: nsbackingstoretype, defer: bool)
现在变成了这样:
init(contentrect: nsrect, stylemask: nswindowstylemask, backing: nsbackingstoretype, defer: bool)
注意看 stylemask 的类型。之前它是一个 int 松散类型(以全局常量方式输入的选项),但是在 xcode 8 中,它以更合理的 optionsettype 类型输入。
不幸的是你不能条件编译方法体相同,而方法签名不同的两个版本。别担心,你可以通过条件编译给类型起别名的方式来解决这个问题!
#if !swift(>=2.3) typealias nswindowstylemask = int #endif
这样你就可以像 swift 2.3 一样在方法签名中使用 nswindowstylemask 了。对于 swift 2.2 而言,这个类型并不存在,nswindowstylemask 只是 int 的一个别名,类型检查器仍然可以完美工作。
非正式 vs 正式协议
swift 2.3 把一些之前的非正式协议 改成了正式协议。
比如,要实现一个 calayer 代理,你只需要继承 nsobject 就可以了,不需要声明它符合 calayerdelegate 协议。事实上,这个协议在 xcode 7 中根本就不存在,只是现在有了。
同样,直接对类声明那行代码做条件编译是不可行的。但是你可以通过在 swift 2.2 中声明虚协议的方式来解决这个问题,就像下面这样:
#if !swift(>=2.3) private protocol calayerdelegate {} #endif class myview: nsview, calayerdelegate { . . . }
(joe groff 提到过可以给 calayerdelegate 起一个 any 的别名——同样的结果,但是没什么开销。)
构建 ios 10 的特性
至此,你的项目可以同时在 xcode 7 和 xcode 8 上进行编译,不需要建立任何分支,这简直太棒了!
现在就是构建 ios 10 特性的时候了,因为已经有了上面所说的各种提示和小技巧,所以这件事情会变得非常简单。但是,还是有一些需要注意的事情:
只用 @available(ios 10, *) 和 #available(ios 10, *) 是不够的。首先,不要在发布的应用中编译任何 ios 10 的代码,因为这样更安全。更重要的是,因为编译器需要检查这些代码,从而保证 api 的使用是安全的,这样就需要注意被调用的 api 是存在的。如果你使用了 ios 9 的 sdk 中不存在的方法或者类型,那么你的代码就无法在 xcode 7 中通过编译。 你需要把所有 ios 10 专用的代码封装在 #if swift(>=2.3) 中(目前你可以认为 swift 2.3 和 ios 10 是相等的)。 大部分时候,你会同时需要条件编译(这样你就不会在 xcode 7 中编译那些不可用的代码) 和 @available/#available (用来通过 xcode 8 的安全检查)。 如果需要处理 ios 10 独有的特性,最简单的方式就是把相关代码抽离到单独的文件中——这样一来你就可以把整个文件的内容都包含在一个 #if swift… 判断中。(在 xcode 7 中这个文件还是可能会被编译器处理到,但是里面的内容都会被忽略。)应用扩展
但问题是,你可能想要在 ios 10 上为你的应用添加一些新的扩展,而不是仅仅给应用本身添加更多的代码。
这就很棘手了。我们可以条件编译我们的代码,但是没有“条件目标“这种东西。
好消息是因为 xcode 7 并不需要真正编译这些目标,所以它并不会向你抱怨什么。(当然,它会发出警告,告诉你项目中有一个目标,它会发布到一个比 base sdk 版本更高的 ios 版本上,但是这不是什么大问题。)
所以方法就是:在每个地方都保留构建目标和它的代码,但是有选择地从应用构建目标的 “target dependencies“和“embed app extensions“ 选项中移除它们。
怎么做呢?我找到的最好方式就是把构建设置中的应用扩展设置成不可用,从而默认兼容 xcode 7。然后只有在使用 xcode 8的时候,才临时添加这些扩展,并且从来不提交这些变动。
如果每次都手动做,听起来太反复无常了(更别说与 ci 和自动化构建的不兼容),别担心,我帮你写了!
安装:
sudo gem install configure_extensions
在提交 xcode 项目的任何变化之前,从应用的构建目标中移除 ios 10 专用的应用扩展:
configure_extensions remove myapp.xcodeproj myapptarget notificationsui intents
然后在 xcode 8 中使用时,把它们添加回来:
configure_extensions add myapp.xcodeproj myapptarget notificationsui intents
你可以把这个放到你的 script/ 文件夹中,然后可以把它加到 xcode 构建的预处理中,也可以加到 git 的预提交 hook 上,或者集成到 ci 和自动化构建中。(更多信息请参照 github)
关于 ios 10 应用扩展需要注意的最后一点:xcode 给这些扩展建立的模板是基于 swift 3 的,而不是 swift 2.3 的代码。所以一定要注意把应用扩展的 “use legacy swift language version“ 构建选项设置为 “yes“,然后把代码用 swift 2.3 重写。
到了9月
到了 9 月份,ios 10 就出来了,这个时候我们需要去掉对 xcode 7 的支持并清理项目!
我给你准备了一个确认清单(记得加入书签,以便后面再来参考):
移除所有 swift 2.2 的代码和不必要的 #if swift(>=2.3) 检查 移除所有过渡处理,比如对 optionalize() 的使用,临时定义的别名,或者虚的协议 移除 configure_extensions 脚本,然后把增加了新应用扩展支持的项目设置提交到代码库 如果你使用了 cocoapods,把它更新,然后移除之前我们添加到 podfile 中 post_install hook(9月份以后基本就用不上了) 更新为 xcode 推荐的项目设置(在侧边栏中选中项目,然后在菜单中选择:editor → validate settings…) 考虑把 provisioning 设置升级,使用新的 provisioning_profile_specifier 把所有的 .xib 和 .storyboard 的设置回滚为 “opens in: latest xcode (8.0)“ 确保所有依赖的 swift 库都更新到了 swift 3。如果没有,考虑自己给 swift 3 这个窗口做点贡献 上面的步骤都搞定之后,就可以把应用更新到 swift 3 了!找到 edit → convert → to current swift syntax…,选择所有的构建目标(记住,你需要一次全部转换好),review 一下 diff,测试,然后提交! 如果你还没有这样做,考虑移除对 ios 8 的支持——这样一来你就可以去掉更多的 @available 检查和其他的条件语句。祝好运!
喎?>推荐阅读
-
同时兼容 Xcode 7 和 Xcode 8
-
让python同时兼容python2和python3的8个技巧分享
-
Swift 适配系列(-1-)同时兼容 Xcode7 和 Xcode8。
-
让 IE6, 7和 8支持CSS3 css3ie6/ie7/ie8兼容
-
同时兼容 Xcode 7 和 Xcode 8
-
让python同时兼容python2和python3的8个技巧分享
-
让python同时兼容python2和python3的8个技巧分享
-
AMD+WIN7+VMware安装MAC OS X 10.6.3 和 Xcode 3.2.2
-
js 兼容多浏览器的回车和鼠标焦点事件代码(IE6/7/8,firefox,chrome)_javascript技巧
-
急急求兼容IE和火狐的网站漂浮物代码!(IE9和IE5、6、7、8解析都不一样的)_html/css_WEB-ITnose