做為 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)行同步。
我先說(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ā)。
說(shuō)這么多你應(yīng)該已經(jīng)很明白了。現(xiàn)在我來(lái)教你如何設(shè)置你的 Xcode 項(xiàng)目,讓它可以在這兩個(gè)版本上運(yùn)行。
首先,在 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 的工作方式在 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)事大吉了。
遍歷所有的 .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 代碼,然后只提交你需要的那幾行。
確保所有的項(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, 你同樣也需要更新 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è)解決方案)
好了,現(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ù)。
就像我之前說(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
)。#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?
。)
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)型檢查器仍然可以完美工作。
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)。)
至此,你的項(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)單。但是,還是有一些需要注意的事情:
@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ò)編譯。#if swift(>=2.3)
中(目前你可以認(rèn)為 Swift 2.3 和 iOS 10 是相等的)。@available/#available
(用來(lái)通過(guò) Xcode 8 的安全檢查)。#if swift…
判斷中。(在 Xcode 7 中這個(gè)文件還是可能會(huì)被編譯器處理到,但是里面的內(nèi)容都會(huì)被忽略。)但問(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 月份,iOS 10 就出來(lái)了,這個(gè)時(shí)候我們需要去掉對(duì) Xcode 7 的支持并清理項(xiàng)目!
我給你準(zhǔn)備了一個(gè)確認(rèn)清單(記得加入書(shū)簽,以便后面再來(lái)參考):
#if swift(>=2.3)
檢查optionalize()
的使用,臨時(shí)定義的別名,或者虛的協(xié)議configure_extensions
腳本,然后把增加了新應(yīng)用擴(kuò)展支持的項(xiàng)目設(shè)置提交到代碼庫(kù)post_install
hook(9月份以后基本就用不上了)PROVISIONING_PROFILE_SPECIFIER
.xib
和 .storyboard
的設(shè)置回滾為 “Opens in: Latest Xcode (8.0)“@available
檢查和其他的條件語(yǔ)句。祝好運(yùn)!
更多建議: