做為 iOS 開發(fā)者,你肯定會對 iOS 10 中的新特性感到無比興奮,并迫不及待地想要在應(yīng)用中進(jìn)行實(shí)踐。雖然你想馬上就動手以便第一時間就能“上船“。但 iOS 10 正式上線卻是幾個月以后的事情,而且在那之前,你還需要保持每幾周就發(fā)布一次的頻率。這個情況聽起來是不是跟你現(xiàn)在的處境很像呢?
當(dāng)然,目前你還不能用 Xcode 8 來編譯需要發(fā)布的應(yīng)用——因?yàn)樗鼰o法通過 App Store 的驗(yàn)證。所以你需要把項(xiàng)目拆分成兩個分支,穩(wěn)定分支和 iOS 10 開發(fā)分支……
而不可避免地是,這爛透了。如果只是在分支上做一點(diǎn)某個特性的開發(fā)還是可以的。但是如果是持續(xù)好幾個月來維護(hù)這個龐大的分支呢?不僅它的整個代碼庫都發(fā)生了變化,而且主分支也一直在演進(jìn),這時候你就會碰到一些不可描述的合并之痛了。我說的是,你有嘗試過處理 .xcodeproj
文件的合并沖突么?
這篇文章的目的就是告訴你如何徹底避免使用分支。對于大部分應(yīng)用而言,只用一個工程文件就同時支持 iOS 9(Xcode 7)和 iOS 10(Xcode 8)是完全可能的。而且即使你不得不使用分支,這些小技巧也可以幫助你減少兩個分支之間的差異,從而更舒服的對它們進(jìn)行同步。
我先說明一點(diǎn):
我們都為 Swift 3 而興奮。它很棒,但是如果你正在讀這篇文章,請別用它(或者暫時別)。雖然它很好,但是它在代碼層面上存在很大的不兼容,比一年前 Swift 2 的不兼容還要嚴(yán)重得多。而且一旦應(yīng)用存在對第三方 Swift 庫的依賴,就得等這些庫都升級到 Swift 3,它才可以跟著升級。
而好消息是,同時也是史無前例的事情,Xcode 8 支持兩個版本的 Swift:2.3 和 3.0。
為了防止你錯過了某些通知,Xcode 7 中的 Swift 2.3 和 Swift 2.3 基本是一致的,除了少數(shù)的 API 調(diào)整(之后會詳細(xì)介紹)。
所以!為了保持兼容性,我們還是用 Swift 2.3 來進(jìn)行開發(fā)。
說這么多你應(yīng)該已經(jīng)很明白了?,F(xiàn)在我來教你如何設(shè)置你的 Xcode 項(xiàng)目,讓它可以在這兩個版本上運(yùn)行。
首先,在 Xcode 7 中打開你的項(xiàng)目。然后打開項(xiàng)目的設(shè)置頁,選中 Build settings 選項(xiàng),然后點(diǎn)擊 “+“來增加一個 User-Defined 設(shè)置項(xiàng):
“SWIFT_VERSION” = “2.3”
這個選項(xiàng)是 Xcode 8 新增的,所以當(dāng)它告訴 Xcode 8 使用 Swift 2.3 時,Xcode 7(實(shí)際上它并沒有 Swift 2.3)會完全忽略這個設(shè)置并繼續(xù)使用 Swift 2.2 來進(jìn)行構(gòu)建。
Framework provisioning 的工作方式在 Xcode 8 上稍有不同——如果是模擬器,它們會按原樣繼續(xù)編譯,而對于真機(jī)會構(gòu)建失敗。
修復(fù)這個問題的方式是,遍歷 Build Settings 中所有的 Framework targets 并增加如下的選項(xiàng),就像 SWIFT_VERSION
:
“PROVISIONING_PROFILE_SPECIFIER” = “ABCDEFGHIJ/“
你需要把“ABCDEFGHIJ“替換成你的團(tuán)隊(duì)ID(你可以在 Apple Developer Portal 中找到它),然后保留最后的斜杠。
這實(shí)際上就是告訴 Xcode 8“嘿,我是來自這個團(tuán)隊(duì)的,你注意下 codesign,好嗎?“,然后 Xcode 7 仍然會忽略這個設(shè)置,這樣就萬事大吉了。
遍歷所有的 .xib
和 .storyboard
文件,打開右側(cè)邊欄,選中第一個選項(xiàng)(File inspector),然后找到“Opens in“設(shè)置項(xiàng)。
大部分情況下它顯示的內(nèi)容是 “Default (7.0)“,把它修改為“Xcode 7.0“。這可以保證即使你是在 Xcode 8 中新建的這個文件,它也只能做一些可以向后兼容 Xcode 7 的變動。
再次提醒一定要注意在 Xcode 8 中對 XIB 所做的改動。因?yàn)樗鼤砑右恍?Xcode 版本相關(guān)的數(shù)據(jù)(不能確定的是應(yīng)用上傳到 App Store 之后這些數(shù)據(jù)是否會被移除掉),而且某些時候它還會嘗試把文件回滾到只支持 Xcode 8 的格式(這是個 bug)。所以我們要盡可能避免在 Xcode 8 中創(chuàng)建 interface 文件,如果實(shí)在沒辦法,那么再每次提交代碼的時候都要仔細(xì) review 代碼,然后只提交你需要的那幾行。
確保所有的項(xiàng)目和構(gòu)建目標(biāo)的 “Base SDK“設(shè)置項(xiàng)都被設(shè)置為 “Latest iOS“。(大部分情況下默認(rèn)設(shè)置就是這樣的,但是還是要再次確認(rèn)下。)這樣一來,Xcode 7 就會針對 iOS 9 來編譯,同時同樣的項(xiàng)目在 Xcode 8 中就可以獲得 iOS 10 的新特性。
如果你用了 CocoaPods, 你同樣也需要更新 Pods 項(xiàng)目的設(shè)置,確保其 Swift 和 provisioning 的設(shè)置是正確的。
同時你也可以通過在 Podfile
文件中添加如下 post-install 代碼的方式來代替手動設(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
來重新生成 Pods 項(xiàng)目。
(如果發(fā)現(xiàn)這個 Pod 不兼容 Swift 2.3,那么你需要為 Xcode 8 單獨(dú)拉一個不同的分支, 這是由 Igor Palaguta 提供的一個解決方案)
好了,現(xiàn)在就可以在 Xcode 8 中打開這個項(xiàng)目了。第一次打開的時候你會被大量的請求所轟炸
Xcode 會提醒你更新到最新版本的 Swift。忽略。
Xcode 還會建議更新項(xiàng)目的設(shè)置為 “推薦設(shè)置“,同樣忽略。
記住,我們已經(jīng)對項(xiàng)目做了設(shè)置,讓它可以在兩個版本下都可以編譯通過。所以現(xiàn)在我們要做的是盡量少做改動,從而保證同時兼容。更重要的是,因?yàn)槲覀儼l(fā)布到 App Store 的文件是同一個,所以我們不希望 .xcodeproj
文件中包含任何 Xcode 8 相關(guān)的數(shù)據(jù)。
就像我之前說過的,Swift 2.3 和 Swift 2.2 是相同的語言。然而,iOS 10 SDK 的 frameworks 已經(jīng)更新了一些 Swift 的注釋。我說的不是大改動(那只是 Swift 3.0 的事情)——但是,Swift 2.3 的一些命名,類型和 API 的可選性還是稍微有些變化。
考慮到你可能會忽略這一點(diǎn), Swift 2.2 就引入了編譯預(yù)處理宏。用法很簡單:
#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 的兩個版本
有兩個需要注意的事項(xiàng):
#if swift(<2.3)
這種寫法是不存在的,只有 >=。如果要表達(dá)相反的意思,你可以寫 #if !swift(>=2.3)
。(如果需要的話你還可以使用 #else
和 #elseif
)。#if
和 #else
必須是有效的 Swift 代碼,例如,你不能只改變方法簽名而不改變方法體。(對于這點(diǎn)后面會有相應(yīng)的處理方案)
Swift 2.3 中很多簽名都把不必要的可選性都去掉了,而有些(比如很多 NSURL
的屬性)也變成 了可選值。
你當(dāng)然也可以用條件編譯來處理這個問題,比如:
#if swift(>=2.3)
let specifier = url.resourceSpecifier ?? ""
#else
let specifier = url.resourceSpecifier
#endif
但是下面的方法可能會小有幫助:
func optionalize<T>(x: T?) -> T? {
return x
}
我知道這有點(diǎn)難理解。也許你看過結(jié)果之后就會容易得多了:
let specifier = optionalize(url.resourceSpecifier) ?? "" // works on both versions!
這樣就發(fā)揮了可選值的封裝優(yōu)勢,從而避免在調(diào)用的時候?qū)憪盒牡臈l件編譯代碼了。optionalize()
方法做的事情就是把任何傳進(jìn)去的值轉(zhuǎn)換成可選值,除非傳入的已經(jīng)是可選值的情況,它就把參數(shù)直接返回。這樣一來,不管 url.resourceSpecifier
是(Xcode 8)或者不是(Xcode 7)可選值,“optionalized“版本永遠(yuǎn)是一樣的。
(更深入地說:在 Swift 里面, Foo
可以被理解為 Foo?
的子類,因?yàn)槟憧梢栽诓粊G失信息的情況下把任何一個 Foo
類型的值封裝成可選值。編譯器一旦知道這點(diǎn),它就允許傳入一個非可選值到一個可選值參數(shù)中-把 Foo
封裝成 Foo?
。)
Swift 2.3 中,一些方法(特別是在 macOS 的 SDK 中)修改了參數(shù)類型。
比如,之前 NSWindow
的構(gòu)造方法是這樣的:
init(contentRect: NSRect, styleMask: Int, backing: NSBackingStoreType, defer: Bool)
現(xiàn)在變成了這樣:
init(contentRect: NSRect, styleMask: NSWindowStyleMask, backing: NSBackingStoreType, defer: Bool)
注意看 styleMask
的類型。之前它是一個 Int 松散類型(以全局常量方式輸入的選項(xiàng)),但是在 Xcode 8 中,它以更合理的 OptionSetType
類型輸入。
不幸的是你不能條件編譯方法體相同,而方法簽名不同的兩個版本。別擔(dān)心,你可以通過條件編譯給類型起別名的方式來解決這個問題!
#if !swift(>=2.3)
typealias NSWindowStyleMask = Int
#endif
這樣你就可以像 Swift 2.3 一樣在方法簽名中使用 NSWindowStyleMask
了。對于 Swift 2.2 而言,這個類型并不存在,NSWindowStyleMask
只是 Int
的一個別名,類型檢查器仍然可以完美工作。
Swift 2.3 把一些之前的非正式協(xié)議 改成了正式協(xié)議。
比如,要實(shí)現(xiàn)一個 CALayer
代理,你只需要繼承 NSObject
就可以了,不需要聲明它符合 CALayerDelegate
協(xié)議。事實(shí)上,這個協(xié)議在 Xcode 7 中根本就不存在,只是現(xiàn)在有了。
同樣,直接對類聲明那行代碼做條件編譯是不可行的。但是你可以通過在 Swift 2.2 中聲明虛協(xié)議的方式來解決這個問題,就像下面這樣:
#if !swift(>=2.3)
private protocol CALayerDelegate {}
#endif
class MyView: NSView, CALayerDelegate { . . . }
(Joe Groff 提到過可以給 CALayerDelegate
起一個 Any
的別名——同樣的結(jié)果,但是沒什么開銷。)
至此,你的項(xiàng)目可以同時在 Xcode 7 和 Xcode 8 上進(jìn)行編譯,不需要建立任何分支,這簡直太棒了!
現(xiàn)在就是構(gòu)建 iOS 10 特性的時候了,因?yàn)橐呀?jīng)有了上面所說的各種提示和小技巧,所以這件事情會變得非常簡單。但是,還是有一些需要注意的事情:
@available(iOS 10, *)
和 #available(iOS 10, *)
是不夠的。首先,不要在發(fā)布的應(yīng)用中編譯任何 iOS 10 的代碼,因?yàn)檫@樣更安全。更重要的是,因?yàn)榫幾g器需要檢查這些代碼,從而保證 API 的使用是安全的,這樣就需要注意被調(diào)用的 API 是存在的。如果你使用了 iOS 9 的 SDK 中不存在的方法或者類型,那么你的代碼就無法在 Xcode 7 中通過編譯。#if swift(>=2.3)
中(目前你可以認(rèn)為 Swift 2.3 和 iOS 10 是相等的)。@available/#available
(用來通過 Xcode 8 的安全檢查)。#if swift…
判斷中。(在 Xcode 7 中這個文件還是可能會被編譯器處理到,但是里面的內(nèi)容都會被忽略。)但問題是,你可能想要在 iOS 10 上為你的應(yīng)用添加一些新的擴(kuò)展,而不是僅僅給應(yīng)用本身添加更多的代碼。
這就很棘手了。我們可以條件編譯我們的代碼,但是沒有“條件目標(biāo)“這種東西。
好消息是因?yàn)?Xcode 7 并不需要真正編譯這些目標(biāo),所以它并不會向你抱怨什么。(當(dāng)然,它會發(fā)出警告,告訴你項(xiàng)目中有一個目標(biāo),它會發(fā)布到一個比 base SDK 版本更高的 iOS 版本上,但是這不是什么大問題。)
所以方法就是:在每個地方都保留構(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的時候,才臨時添加這些擴(kuò)展,并且從來不提交這些變動。
如果每次都手動做,聽起來太反復(fù)無常了(更別說與 CI 和自動化構(gòu)建的不兼容),別擔(dān)心,我?guī)湍銓懥?a rel="external nofollow" target="_blank" rel="external nofollow" target="_blank" >一個腳本!
安裝:
sudo gem install configure_extensions
在提交 Xcode 項(xiàng)目的任何變化之前,從應(yīng)用的構(gòu)建目標(biāo)中移除 iOS 10 專用的應(yīng)用擴(kuò)展:
configure_extensions remove MyApp.xcodeproj MyAppTarget NotificationsUI Intents
然后在 Xcode 8 中使用時,把它們添加回來:
configure_extensions add MyApp.xcodeproj MyAppTarget NotificationsUI Intents
你可以把這個放到你的 script/
文件夾中,然后可以把它加到 Xcode 構(gòu)建的預(yù)處理中,也可以加到 Git 的預(yù)提交 hook 上,或者集成到 CI 和自動化構(gòu)建系統(tǒ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 重寫。
到了 9 月份,iOS 10 就出來了,這個時候我們需要去掉對 Xcode 7 的支持并清理項(xiàng)目!
我給你準(zhǔn)備了一個確認(rèn)清單(記得加入書簽,以便后面再來參考):
#if swift(>=2.3)
檢查optionalize()
的使用,臨時定義的別名,或者虛的協(xié)議configure_extensions
腳本,然后把增加了新應(yīng)用擴(kuò)展支持的項(xiàng)目設(shè)置提交到代碼庫post_install
hook(9月份以后基本就用不上了)PROVISIONING_PROFILE_SPECIFIER
.xib
和 .storyboard
的設(shè)置回滾為 “Opens in: Latest Xcode (8.0)“@available
檢查和其他的條件語句。祝好運(yùn)!
更多建議: