同時兼容 Xcode7 和 Xcode8

做者:radex.io,原文連接,原文日期:2016-07-28
譯者:ckitakishi;校對:Channe;定稿:CMBhtml

做爲一名 iOS 開發者,你必定對 iOS 10 帶來的新特性感到無比興奮,併火燒眉毛地想要在應用中實踐。雖然你想立刻就動手以便第一時間就能「上車」。但 iOS 10 正式上線倒是幾個月之後的事情,在那以前,你不得不保持每幾周就爲應用發佈一個新版本的頻率。這個狀況聽起來是否是跟你如今的處境很像呢?ios

固然,目前你還不能用 Xcode 8 來編譯須要發佈的應用——由於它沒法經過 App Store 的驗證。因此你須要把項目拆分紅兩個分支,穩定分支和 iOS 10 開發分支……git

而不可避免地是,這爛透了。若是隻是暫時在分支上作一點某個特性的開發並沒有傷大雅。可是隨着整個代碼庫的改變,主分支的演進,持續好幾個月來維護這樣一個龐大的分支的時候,你就會漸漸遇到一些不可描述的合併之痛。個人意思是,你嘗試過處理 .xcodeproj 文件的合併衝突麼?github

這篇文章的目的就是告訴你如何完全避免使用分支。對於大部分應用而言,只用一個工程文件就同時支持 iOS 9(Xcode 7)和 iOS 10(Xcode 8)是徹底可能的。並且即便你不得不使用分支,這些小技巧也能夠幫助你減小兩個分支之間的差別,從而更舒服地對它們進行同步。swift

你用的是 Swift 2.3

我先說明一點:xcode

咱們都爲 Swift 3 的到來而興奮。它很棒,可是若是你正在讀這篇文章,請別用它(或者說暫時別)。雖然它足夠好,可是在代碼層面上存在的不兼容,比一年前的 Swift 2 還要嚴重得多。並且一旦應用存在對第三方 Swift 庫的依賴,就得等這些庫都升級到 Swift 3,它才能夠跟着升級。安全

而好消息是,前所未有地,Xcode 8 支持兩個版本的 Swift:2.3 和 3.0。app

爲了防止你因錯過了發佈會而不太瞭解現狀,我想再說一遍,除了少數的 API 有所調整(以後會詳細介紹)之外,Xcode 7 中的 Swift 2.2 和 Swift 2.3 基本是一致的。ide

因此!爲了保持兼容性,咱們仍是用 Swift 2.3 來進行開發。函數

Xcode 的設置

說這麼多你應該已經很明白了。如今讓我來告訴你如何設置 Xcode 項目,讓它能夠在這兩個版本上運行。

Swift 版本

首先,在 Xcode 7 中打開你的項目,選中項目設置頁的 Build settings 選項,而後點擊 「+「 來增長一個 User-Defined 設置項:

「SWIFT_VERSION」 = 「2.3」

這個選項是 Xcode 8 新增的,所以,即便它表示該項目使用 Swift 2.3,Xcode 7(實際上它並無 Swift 2.3)也會徹底忽略這個設置並繼續使用 Swift 2.2 來進行構建。

Framework provisioning

Framework provisioning 的工做方式在 Xcode 8 上稍有不一樣 —— 若是是模擬器,它們會按原樣繼續編譯,而對於真機會構建失敗。

要解決這個問題,能夠像設置 SWIFT_VERSION 時所作的同樣,遍歷 Build Settings 中全部的 Framework targets 並增長以下選項:

「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 diff,而且只提交你須要的那幾行。

SDK 版本

確保項目全部構建目標的 「Base SDK「 設置項都已被設置爲了 「Latest iOS「。(大部分狀況下默認設置就是這樣的,可是仍是要再次確認下。)這樣一來,Xcode 7 就會針對 iOS 9 來進行編譯,可是你能夠在 Xcode 8 中打開一樣的項目並使用 iOS 10 的新特性。

CocoaPods 設置

若是你正在使用 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<T>(x: T?) -> T? {
    return x
}

我知道這有點難理解。也許你看過結果以後就會容易得多了:

let specifier = optionalize(url.resourceSpecifier) ?? "" // 適用於兩個版本!

這樣就發揮了可選值的封裝優點,從而避免在調用的時候寫噁心的條件編譯代碼了。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 特性的時候了,由於已經有了上面所說的各類提示和小技巧,因此這件事情會變得很是簡單。可是,仍是有一些須要注意的事情:

  1. 只用 @available(iOS 10, *)if #available(iOS 10, *) 是不夠的。首先,不要在發佈的應用中編譯任何 iOS 10 的代碼,由於這樣比較安全。更爲重要的緣由是,編譯器須要檢查這些代碼,從而保證 API 的使用是安全的,這樣就須要注意被調用的 API 是存在的。若是你使用了 iOS 9 的 SDK 中不存在的方法或者類型,那麼你的代碼就沒法在 Xcode 7 中經過編譯。

  2. 你須要把全部 iOS 10 專用的代碼封裝在 #if swift(>=2.3) 中(目前你能夠認爲 Swift 2.3 和 iOS 10 是相等的)。

  3. 大部分時候,你會同時須要條件編譯(這樣你就不會在 Xcode 7 中編譯那些不可用的代碼) 和 @available/#available(用來經過 Xcode 8 的安全檢查)。

  4. 若是須要處理 iOS 10 獨有的特性,最簡單的方式就是把相關代碼抽離到單獨的文件中 —— 這樣一來你就能夠把整個文件的內容都包含在一個 #if swift… 判斷中(在 Xcode 7 中這個文件仍是可能會被編譯器處理,可是裏面的內容都會被忽略。)

應用擴展

但問題是,你可能想要在 iOS 10 上爲你的應用添加一些新的擴展,而不是僅僅給應用自己添加更多的代碼。

這就很棘手了。咱們能夠條件編譯咱們的代碼,可是沒有「條件目標「這種東西。

好消息是,只要 Xcode 7 無需實際地編譯這些目標,它就不會向你抱怨什麼。(是的,它可能會發出警告,告訴你項目包含一個目標,用於配置將應用部署到一個比基礎 SDK 版本更高的 iOS 上,它會發布到一個比 base SDK 版本更高的 iOS 版本上,可是這不是什麼大問題。)

因此方法就是:在每一個地方都保留構建目標和它的代碼,可是有選擇地從應用構建目標 build phases 標籤頁的 「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 檢查和其餘的條件語句。

祝好運!

本文由 SwiftGG 翻譯組翻譯,已經得到做者翻譯受權,最新文章請訪問 http://swift.gg

相關文章
相關標籤/搜索