[譯]同時(shí)兼容 Xcode 7 和 Xcode 8

2018-06-19 14:56 更新

做為 iOS 開(kāi)發(fā)者,你肯定會(huì)對(duì) iOS 10 中的新特性感到無(wú)比興奮,并迫不及待地想要在應(yīng)用中進(jìn)行實(shí)踐。雖然你想馬上就動(dòng)手以便第一時(shí)間就能“上船“。但 iOS 10 正式上線(xiàn)卻是幾個(gè)月以后的事情,而且在那之前,你還需要保持每幾周就發(fā)布一次的頻率。這個(gè)情況聽(tīng)起來(lái)是不是跟你現(xiàn)在的處境很像呢?

當(dāng)然,目前你還不能用 Xcode 8 來(lái)編譯需要發(fā)布的應(yīng)用——因?yàn)樗鼰o(wú)法通過(guò) App Store 的驗(yàn)證。所以你需要把項(xiàng)目拆分成兩個(gè)分支,穩(wěn)定分支和 iOS 10 開(kāi)發(fā)分支……

而不可避免地是,這爛透了。如果只是在分支上做一點(diǎn)某個(gè)特性的開(kāi)發(fā)還是可以的。但是如果是持續(xù)好幾個(gè)月來(lái)維護(hù)這個(gè)龐大的分支呢?不僅它的整個(gè)代碼庫(kù)都發(fā)生了變化,而且主分支也一直在演進(jìn),這時(shí)候你就會(huì)碰到一些不可描述的合并之痛了。我說(shuō)的是,你有嘗試過(guò)處理 .xcodeproj 文件的合并沖突么?

這篇文章的目的就是告訴你如何徹底避免使用分支。對(duì)于大部分應(yīng)用而言,只用一個(gè)工程文件就同時(shí)支持 iOS 9(Xcode 7)和 iOS 10(Xcode 8)是完全可能的。而且即使你不得不使用分支,這些小技巧也可以幫助你減少兩個(gè)分支之間的差異,從而更舒服的對(duì)它們進(jìn)行同步。

你要用的是 Swift 2.3

我先說(shuō)明一點(diǎn):

我們都為 Swift 3 而興奮。它很棒,但是如果你正在讀這篇文章,請(qǐng)別用它(或者暫時(shí)別)。雖然它很好,但是它在代碼層面上存在很大的不兼容,比一年前 Swift 2 的不兼容還要嚴(yán)重得多。而且一旦應(yīng)用存在對(duì)第三方 Swift 庫(kù)的依賴(lài),就得等這些庫(kù)都升級(jí)到 Swift 3,它才可以跟著升級(jí)。

而好消息是,同時(shí)也是史無(wú)前例的事情,Xcode 8 支持兩個(gè)版本的 Swift:2.3 和 3.0。

為了防止你錯(cuò)過(guò)了某些通知,Xcode 7 中的 Swift 2.3 和 Swift 2.3 基本是一致的,除了少數(shù)的 API 調(diào)整(之后會(huì)詳細(xì)介紹)。

所以!為了保持兼容性,我們還是用 Swift 2.3 來(lái)進(jìn)行開(kāi)發(fā)。

Xcode 的設(shè)置

說(shuō)這么多你應(yīng)該已經(jīng)很明白了。現(xiàn)在我來(lái)教你如何設(shè)置你的 Xcode 項(xiàng)目,讓它可以在這兩個(gè)版本上運(yùn)行。

Swift version

這里寫(xiě)圖片描述

首先,在 Xcode 7 中打開(kāi)你的項(xiàng)目。然后打開(kāi)項(xiàng)目的設(shè)置頁(yè),選中 Build settings 選項(xiàng),然后點(diǎn)擊 “+“來(lái)增加一個(gè) User-Defined 設(shè)置項(xiàng):

“SWIFT_VERSION” = “2.3”

這個(gè)選項(xiàng)是 Xcode 8 新增的,所以當(dāng)它告訴 Xcode 8 使用 Swift 2.3 時(shí),Xcode 7(實(shí)際上它并沒(méi)有 Swift 2.3)會(huì)完全忽略這個(gè)設(shè)置并繼續(xù)使用 Swift 2.2 來(lái)進(jìn)行構(gòu)建。

Framework provisioning

Framework provisioning 的工作方式在 Xcode 8 上稍有不同——如果是模擬器,它們會(huì)按原樣繼續(xù)編譯,而對(duì)于真機(jī)會(huì)構(gòu)建失敗。

修復(fù)這個(gè)問(wèn)題的方式是,遍歷 Build Settings 中所有的 Framework targets 并增加如下的選項(xiàng),就像 SWIFT_VERSION

“PROVISIONING_PROFILE_SPECIFIER” = “ABCDEFGHIJ/“

你需要把“ABCDEFGHIJ“替換成你的團(tuán)隊(duì)ID(你可以在 Apple Developer Portal 中找到它),然后保留最后的斜杠。

這實(shí)際上就是告訴 Xcode 8“嘿,我是來(lái)自這個(gè)團(tuán)隊(duì)的,你注意下 codesign,好嗎?“,然后 Xcode 7 仍然會(huì)忽略這個(gè)設(shè)置,這樣就萬(wàn)事大吉了。

Interface Builder

遍歷所有的 .xib.storyboard 文件,打開(kāi)右側(cè)邊欄,選中第一個(gè)選項(xiàng)(File inspector),然后找到“Opens in“設(shè)置項(xiàng)。

大部分情況下它顯示的內(nèi)容是 “Default (7.0)“,把它修改為“Xcode 7.0“。這可以保證即使你是在 Xcode 8 中新建的這個(gè)文件,它也只能做一些可以向后兼容 Xcode 7 的變動(dòng)。

再次提醒一定要注意在 Xcode 8 中對(duì) XIB 所做的改動(dòng)。因?yàn)樗鼤?huì)添加一些 Xcode 版本相關(guān)的數(shù)據(jù)(不能確定的是應(yīng)用上傳到 App Store 之后這些數(shù)據(jù)是否會(huì)被移除掉),而且某些時(shí)候它還會(huì)嘗試把文件回滾到只支持 Xcode 8 的格式(這是個(gè) bug)。所以我們要盡可能避免在 Xcode 8 中創(chuàng)建 interface 文件,如果實(shí)在沒(méi)辦法,那么再每次提交代碼的時(shí)候都要仔細(xì) review 代碼,然后只提交你需要的那幾行。

SDK version

確保所有的項(xiàng)目和構(gòu)建目標(biāo)的 “Base SDK“設(shè)置項(xiàng)都被設(shè)置為 “Latest iOS“。(大部分情況下默認(rèn)設(shè)置就是這樣的,但是還是要再次確認(rèn)下。)這樣一來(lái),Xcode 7 就會(huì)針對(duì) iOS 9 來(lái)編譯,同時(shí)同樣的項(xiàng)目在 Xcode 8 中就可以獲得 iOS 10 的新特性。

CocoaPods settings

如果你用了 CocoaPods, 你同樣也需要更新 Pods 項(xiàng)目的設(shè)置,確保其 Swift 和 provisioning 的設(shè)置是正確的。

同時(shí)你也可以通過(guò)在 Podfile 文件中添加如下 post-install 代碼的方式來(lái)代替手動(dòng)設(shè)置:

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 替換成你的團(tuán)隊(duì) ID。然后運(yùn)行 pod install 來(lái)重新生成 Pods 項(xiàng)目。

(如果發(fā)現(xiàn)這個(gè) Pod 不兼容 Swift 2.3,那么你需要為 Xcode 8 單獨(dú)拉一個(gè)不同的分支, 這是由 Igor Palaguta 提供的一個(gè)解決方案

在 Xcode 8 中打開(kāi)

好了,現(xiàn)在就可以在 Xcode 8 中打開(kāi)這個(gè)項(xiàng)目了。第一次打開(kāi)的時(shí)候你會(huì)被大量的請(qǐng)求所轟炸

Xcode 會(huì)提醒你更新到最新版本的 Swift。忽略。

Xcode 還會(huì)建議更新項(xiàng)目的設(shè)置為 “推薦設(shè)置“,同樣忽略。

記住,我們已經(jīng)對(duì)項(xiàng)目做了設(shè)置,讓它可以在兩個(gè)版本下都可以編譯通過(guò)。所以現(xiàn)在我們要做的是盡量少做改動(dòng),從而保證同時(shí)兼容。更重要的是,因?yàn)槲覀儼l(fā)布到 App Store 的文件是同一個(gè),所以我們不希望 .xcodeproj 文件中包含任何 Xcode 8 相關(guān)的數(shù)據(jù)。

處理 Swift 2.3 的差異

就像我之前說(shuō)過(guò)的,Swift 2.3 和 Swift 2.2 是相同的語(yǔ)言。然而,iOS 10 SDK 的 frameworks 已經(jīng)更新了一些 Swift 的注釋。我說(shuō)的不是大改動(dòng)(那只是 Swift 3.0 的事情)——但是,Swift 2.3 的一些命名,類(lèi)型和 API 的可選性還是稍微有些變化。

條件編譯

考慮到你可能會(huì)忽略這一點(diǎn), Swift 2.2 就引入了編譯預(yù)處理宏。用法很簡(jiǎn)單:

#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

太棒了!一個(gè)文件,不需要分支就同時(shí)兼容了 Xcode 的兩個(gè)版本

有兩個(gè)需要注意的事項(xiàng):

  • #if swift(<2.3) 這種寫(xiě)法是不存在的,只有 >=。如果要表達(dá)相反的意思,你可以寫(xiě) #if !swift(>=2.3)。(如果需要的話(huà)你還可以使用 #else#elseif)。
  • 和 C 的預(yù)處理不同,#if#else 必須是有效的 Swift 代碼,例如,你不能只改變方法簽名而不改變方法體。(對(duì)于這點(diǎn)后面會(huì)有相應(yīng)的處理方案)

可選性的變化

Swift 2.3 中很多簽名都把不必要的可選性都去掉了,而有些(比如很多 NSURL 的屬性)也變成 了可選值。

你當(dāng)然也可以用條件編譯來(lái)處理這個(gè)問(wèn)題,比如:

#if swift(>=2.3)
let specifier = url.resourceSpecifier ?? ""
#else
let specifier = url.resourceSpecifier
#endif

但是下面的方法可能會(huì)小有幫助:

func optionalize<T>(x: T?) -> T? {
    return x
}

我知道這有點(diǎn)難理解。也許你看過(guò)結(jié)果之后就會(huì)容易得多了:

let specifier = optionalize(url.resourceSpecifier) ?? "" // works on both versions!

這樣就發(fā)揮了可選值的封裝優(yōu)勢(shì),從而避免在調(diào)用的時(shí)候?qū)憪盒牡臈l件編譯代碼了。optionalize() 方法做的事情就是把任何傳進(jìn)去的值轉(zhuǎn)換成可選值,除非傳入的已經(jīng)是可選值的情況,它就把參數(shù)直接返回。這樣一來(lái),不管 url.resourceSpecifier 是(Xcode 8)或者不是(Xcode 7)可選值,“optionalized“版本永遠(yuǎn)是一樣的。

(更深入地說(shuō):在 Swift 里面, Foo 可以被理解為 Foo? 的子類(lèi),因?yàn)槟憧梢栽诓粊G失信息的情況下把任何一個(gè) Foo 類(lèi)型的值封裝成可選值。編譯器一旦知道這點(diǎn),它就允許傳入一個(gè)非可選值到一個(gè)可選值參數(shù)中-把 Foo 封裝成 Foo?。)

用別名來(lái)拯救方法簽名的變化

Swift 2.3 中,一些方法(特別是在 macOS 的 SDK 中)修改了參數(shù)類(lèi)型。

比如,之前 NSWindow 的構(gòu)造方法是這樣的:

init(contentRect: NSRect, styleMask: Int, backing: NSBackingStoreType, defer: Bool)

現(xiàn)在變成了這樣:

init(contentRect: NSRect, styleMask: NSWindowStyleMask, backing: NSBackingStoreType, defer: Bool)

注意看 styleMask 的類(lèi)型。之前它是一個(gè) Int 松散類(lèi)型(以全局常量方式輸入的選項(xiàng)),但是在 Xcode 8 中,它以更合理的 OptionSetType 類(lèi)型輸入。

不幸的是你不能條件編譯方法體相同,而方法簽名不同的兩個(gè)版本。別擔(dān)心,你可以通過(guò)條件編譯給類(lèi)型起別名的方式來(lái)解決這個(gè)問(wèn)題!

#if !swift(>=2.3)
typealias NSWindowStyleMask = Int
#endif

這樣你就可以像 Swift 2.3 一樣在方法簽名中使用 NSWindowStyleMask 了。對(duì)于 Swift 2.2 而言,這個(gè)類(lèi)型并不存在,NSWindowStyleMask 只是 Int 的一個(gè)別名,類(lèi)型檢查器仍然可以完美工作。

非正式 vs 正式協(xié)議

Swift 2.3 把一些之前的非正式協(xié)議 改成了正式協(xié)議。

比如,要實(shí)現(xiàn)一個(gè) CALayer 代理,你只需要繼承 NSObject 就可以了,不需要聲明它符合 CALayerDelegate 協(xié)議。事實(shí)上,這個(gè)協(xié)議在 Xcode 7 中根本就不存在,只是現(xiàn)在有了。

同樣,直接對(duì)類(lèi)聲明那行代碼做條件編譯是不可行的。但是你可以通過(guò)在 Swift 2.2 中聲明虛協(xié)議的方式來(lái)解決這個(gè)問(wèn)題,就像下面這樣:

#if !swift(>=2.3)
private protocol CALayerDelegate {}
#endif


class MyView: NSView, CALayerDelegate { . . . }

Joe Groff 提到過(guò)可以給 CALayerDelegate 起一個(gè) Any 的別名——同樣的結(jié)果,但是沒(méi)什么開(kāi)銷(xiāo)。)

構(gòu)建 iOS 10 的特性

至此,你的項(xiàng)目可以同時(shí)在 Xcode 7 和 Xcode 8 上進(jìn)行編譯,不需要建立任何分支,這簡(jiǎn)直太棒了!

現(xiàn)在就是構(gòu)建 iOS 10 特性的時(shí)候了,因?yàn)橐呀?jīng)有了上面所說(shuō)的各種提示和小技巧,所以這件事情會(huì)變得非常簡(jiǎn)單。但是,還是有一些需要注意的事情:

  1. 只用 @available(iOS 10, *)#available(iOS 10, *) 是不夠的。首先,不要在發(fā)布的應(yīng)用中編譯任何 iOS 10 的代碼,因?yàn)檫@樣更安全。更重要的是,因?yàn)榫幾g器需要檢查這些代碼,從而保證 API 的使用是安全的,這樣就需要注意被調(diào)用的 API 是存在的。如果你使用了 iOS 9 的 SDK 中不存在的方法或者類(lèi)型,那么你的代碼就無(wú)法在 Xcode 7 中通過(guò)編譯。
  2. 你需要把所有 iOS 10 專(zhuān)用的代碼封裝在 #if swift(>=2.3) 中(目前你可以認(rèn)為 Swift 2.3 和 iOS 10 是相等的)。
  3. 大部分時(shí)候,你會(huì)同時(shí)需要條件編譯(這樣你就不會(huì)在 Xcode 7 中編譯那些不可用的代碼) 和 @available/#available (用來(lái)通過(guò) Xcode 8 的安全檢查)。
  4. 如果需要處理 iOS 10 獨(dú)有的特性,最簡(jiǎn)單的方式就是把相關(guān)代碼抽離到單獨(dú)的文件中——這樣一來(lái)你就可以把整個(gè)文件的內(nèi)容都包含在一個(gè) #if swift… 判斷中。(在 Xcode 7 中這個(gè)文件還是可能會(huì)被編譯器處理到,但是里面的內(nèi)容都會(huì)被忽略。)

應(yīng)用擴(kuò)展

但問(wèn)題是,你可能想要在 iOS 10 上為你的應(yīng)用添加一些新的擴(kuò)展,而不是僅僅給應(yīng)用本身添加更多的代碼。

這就很棘手了。我們可以條件編譯我們的代碼,但是沒(méi)有“條件目標(biāo)“這種東西。

好消息是因?yàn)?Xcode 7 并不需要真正編譯這些目標(biāo),所以它并不會(huì)向你抱怨什么。(當(dāng)然,它會(huì)發(fā)出警告,告訴你項(xiàng)目中有一個(gè)目標(biāo),它會(huì)發(fā)布到一個(gè)比 base SDK 版本更高的 iOS 版本上,但是這不是什么大問(wèn)題。)

所以方法就是:在每個(gè)地方都保留構(gòu)建目標(biāo)和它的代碼,但是有選擇地從應(yīng)用構(gòu)建目標(biāo)的 “Target Dependencies“和“Embed App Extensions“ 選項(xiàng)中移除它們。

怎么做呢?我找到的最好方式就是把構(gòu)建設(shè)置中的應(yīng)用擴(kuò)展設(shè)置成不可用,從而默認(rèn)兼容 Xcode 7。然后只有在使用 Xcode 8的時(shí)候,才臨時(shí)添加這些擴(kuò)展,并且從來(lái)不提交這些變動(dòng)。

如果每次都手動(dòng)做,聽(tīng)起來(lái)太反復(fù)無(wú)常了(更別說(shuō)與 CI 和自動(dòng)化構(gòu)建的不兼容),別擔(dān)心,我?guī)湍銓?xiě)了一個(gè)腳本!

安裝:

sudo gem install configure_extensions

在提交 Xcode 項(xiàng)目的任何變化之前,從應(yīng)用的構(gòu)建目標(biāo)中移除 iOS 10 專(zhuān)用的應(yīng)用擴(kuò)展:

configure_extensions remove MyApp.xcodeproj MyAppTarget NotificationsUI Intents

然后在 Xcode 8 中使用時(shí),把它們添加回來(lái):

configure_extensions add MyApp.xcodeproj MyAppTarget NotificationsUI Intents

你可以把這個(gè)放到你的 script/ 文件夾中,然后可以把它加到 Xcode 構(gòu)建的預(yù)處理中,也可以加到 Git 的預(yù)提交 hook 上,或者集成到 CI 和自動(dòng)化構(gòu)建系統(tǒng)中。(更多信息請(qǐng)參照 GitHub

關(guān)于 iOS 10 應(yīng)用擴(kuò)展需要注意的最后一點(diǎn):Xcode 給這些擴(kuò)展建立的模板是基于 Swift 3 的,而不是 Swift 2.3 的代碼。所以一定要注意把應(yīng)用擴(kuò)展的 “Use Legacy Swift Language Version“ 構(gòu)建選項(xiàng)設(shè)置為 “Yes“,然后把代碼用 Swift 2.3 重寫(xiě)。

到了9月

到了 9 月份,iOS 10 就出來(lái)了,這個(gè)時(shí)候我們需要去掉對(duì) Xcode 7 的支持并清理項(xiàng)目!

我給你準(zhǔn)備了一個(gè)確認(rèn)清單(記得加入書(shū)簽,以便后面再來(lái)參考):

  • 移除所有 Swift 2.2 的代碼和不必要的 #if swift(>=2.3) 檢查
  • 移除所有過(guò)渡處理,比如對(duì) optionalize() 的使用,臨時(shí)定義的別名,或者虛的協(xié)議
  • 移除 configure_extensions 腳本,然后把增加了新應(yīng)用擴(kuò)展支持的項(xiàng)目設(shè)置提交到代碼庫(kù)
  • 如果你使用了 CocoaPods,把它更新,然后移除之前我們添加到 Podfile 中 post_install hook(9月份以后基本就用不上了)
  • 更新為 Xcode 推薦的項(xiàng)目設(shè)置(在側(cè)邊欄中選中項(xiàng)目,然后在菜單中選擇:Editor → Validate Settings…)
  • 考慮把 provisioning 設(shè)置升級(jí),使用新的 PROVISIONING_PROFILE_SPECIFIER
  • 把所有的 .xib.storyboard 的設(shè)置回滾為 “Opens in: Latest Xcode (8.0)“
  • 確保所有依賴(lài)的 Swift 庫(kù)都更新到了 Swift 3。如果沒(méi)有,考慮自己給 Swift 3 這個(gè)窗口做點(diǎn)貢獻(xiàn)
  • 上面的步驟都搞定之后,就可以把應(yīng)用更新到 Swift 3 了!找到 Edit → Convert → To Current Swift Syntax…,選擇所有的構(gòu)建目標(biāo)(記住,你需要一次全部轉(zhuǎn)換好),review 一下 diff,測(cè)試,然后提交!
  • 如果你還沒(méi)有這樣做,考慮移除對(duì) iOS 8 的支持——這樣一來(lái)你就可以去掉更多的 @available 檢查和其他的條件語(yǔ)句。

祝好運(yùn)!

以上內(nèi)容是否對(duì)您有幫助:
在線(xiàn)筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)