你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發綜合 >> Swift 適配系列(-1-)同時兼容 Xcode7 和 Xcode8。

Swift 適配系列(-1-)同時兼容 Xcode7 和 Xcode8。

編輯: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。

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

所以!為了保持兼容性,我們還是用 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(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 特性的時候了,因為已經有了上面所說的各種提示和小技巧,所以這件事情會變得非常簡單。但是,還是有一些需要注意的事情:

只用 @available(iOS 10, *) 和 if #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 無需實際地編譯這些目標,它就不會向你抱怨什麼。(是的,它可能會發出警告,告訴你項目包含一個目標,用於配置將應用部署到一個比基礎 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 重寫。

發布Xcode 8

到了 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