你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發綜合 >> 同時兼容 Xcode 7 和 Xcode 8

同時兼容 Xcode 7 和 Xcode 8

編輯:IOS開發綜合

做為 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 設置項:<喎?/kf/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 檢查和其他的條件語句。

祝好運!

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved