(原文:How to Create a Framework for iOS 作者:Sam Davies 譯者:Mr_cyz )
怎樣讓其他開發(fā)者更方便地去復用控件?
如果你想將你開發(fā)的控件與別人分享,一種方法是直接提供源代碼文件。然而,這種方法并不是很優(yōu)雅。它會暴露所有的實現(xiàn)細節(jié),而這些實現(xiàn)你可能并不想開源出來。此外,開發(fā)者也可能并不想看到你的所有代碼,因為他們可能僅僅希望將你的這份漂亮代碼的一部分植入自己的應(yīng)用中。
另一種方法是將你的代碼編譯成靜態(tài)庫(library),讓其他開發(fā)者添加到自己的項目中。然而,這需要你一并公布所有的公開的頭文件,實在是非常不方便。
你需要一種簡單的方法來編譯你的代碼,這種方法應(yīng)該使得你的代碼易分享,并且在多個工程中易復用。你需要的是一種方法來打包你的靜態(tài)庫,將所有的頭文件放到一個單元中,這樣你就可以立刻將其加入到你的項目中并使用。
非常幸運,這正是本篇教程所要解決的問題。你將會學到制作并使用Framework,幫助你解決這個頭疼的問題。OS X完美地支持這一點,因為Xcode就提供了一個項目模板,包含著默認構(gòu)建目標(target)和可以容納類似于圖片、聲音、字體等資源的文件。你可以為iOS創(chuàng)建Framework,不過這是一個比較復雜的手工活,如果你跟著教程走,你將學到怎么樣跨過路障,順利地完成Framework的創(chuàng)建。
當你跟著這篇教程走完后,你將能夠:
使用Xcode構(gòu)建一個基本的靜態(tài)庫工程。
依賴于該靜態(tài)庫工程構(gòu)建一款應(yīng)用。
掌握如何將靜態(tài)庫工程轉(zhuǎn)換為完整的、合格的Framework。
最終,你將看到如何將一個圖像文件同F(xiàn)ramework一起打包到resource bundle下。
開始
這篇教程的主要目的是解釋怎么樣在你的iOS工程中創(chuàng)建并使用一個Framework。所以,不像其他網(wǎng)站上的教程,這篇教程將只使用一小部分Objective-C代碼,并且這一小部分主要是為了說明我們將會遇到的一些概念。
從這里下載可用的資源文件RWKnobControl。如果你在Creating a Static Library Project 這篇文章中完成了創(chuàng)建第一個項目的過程,這里你將會看到怎么樣使用去它們。
在創(chuàng)建本工程時,你將要創(chuàng)建的所有的代碼和項目文件都可以在Github上找到。對于本篇教程中每個創(chuàng)建階段都有不同的commit。
什么是Framework?
Framework是資源的集合,將靜態(tài)庫和其頭文件包含到一個結(jié)構(gòu)中,讓Xcode可以方便地把它納入到你的項目中。
在OS X上,可能會創(chuàng)建一個動態(tài)連接(Dynamically Linked)的framework。通過動態(tài)連接,framework可以更新,不需要應(yīng)用重新連接。在運行時,庫中代碼的一份拷貝被分享出來,整個工程都可以使用它,因此,這樣減少了內(nèi)存消耗,提高了系統(tǒng)的性能。正如你看到的,這是一個功能強大的特性。
在iOS上,你不能用這種方式添加為系統(tǒng)添加自定義的framework,因此僅有的動態(tài)鏈接的framework只能是Apple提供的那些。(編者注:在iOS 8中已加入此特性,開發(fā)者可以使用第三方的動態(tài)框架)
然而,這并不意味著framework對于iOS而言是無關(guān)緊要的,靜態(tài)連接的framework依然可以打包代碼,使其在不同的應(yīng)用中復用。
由于framework本質(zhì)上是靜態(tài)庫的“一站式采購點”,因此在本篇教程中你所做的第一件事就是創(chuàng)建并使用靜態(tài)庫。當跟著教程走到如何創(chuàng)建framework時,你就能明白你所做的一切了,整體思路也不會那么煙霧繚繞了。
創(chuàng)建一個靜態(tài)庫工程
打開Xcode,點擊File\New\Project,選擇iOS\Framework and Library\Cocoa Touch Static Library新建一個靜態(tài)庫工程.
將工程命名為RWUIControls,然后將工程保存到一個空目錄下。
一個靜態(tài)庫工程由頭文件和實現(xiàn)文件組成,這些文件將被編譯為庫本身。
為了方便其他開發(fā)者使用你的庫和framework,你將進行一些操作,讓他們僅需要導入一個頭文件便可以訪問所有你想公開的類。
當創(chuàng)建靜態(tài)庫工程時,Xcode會自動添加RWUIControls.h和RWUIControls.m。你不需要實現(xiàn)文件,因此右鍵單擊RWUIControls.m選擇delete,將它刪除到廢紙簍中。
打開RWUIControls.h,將所有內(nèi)容替換為:
#import < UIKit/UIKit.h>
導入UIKit的頭文件,這是創(chuàng)建一個庫所需要的。當你在創(chuàng)建不同的組成類時,你將會將它們添加到這個文件中,確保它們能夠被庫的使用者獲取到。
你所構(gòu)建的項目依賴于UIKit,然而Xcode的靜態(tài)庫工程不會自動連接到UIKit。要解決這個問題,就要將UIKit作為依賴庫添加到工程中。在工程導航欄中選擇工程名,然后在中央面板中選擇RWUIControls目標。
點擊BuildPhases,展開Link Binary with Libraries這一部分,點擊+添加一個新的framework,找到UIKit.framework,點擊add添加進來。
如果不結(jié)合頭文件,靜態(tài)庫是沒有用的,靜態(tài)庫編譯一組文件,在這些文件中類和方法都以二進制數(shù)據(jù)的形式存在。在你創(chuàng)建的庫中,有些類將能夠被公開訪問到,有些類只能由庫內(nèi)部訪問并使用。
接下來,你需要在build欄中添加新的phase,來包含所有頭文件,并將它們放到編譯器可以獲取到的某個地方。然后,你將會拷貝這些到你的framework中。
依然是在Xcode的Build Phases界面,選擇Editor\Add Build Phase\Add Copy Headers Build Phase。
Note:如果你發(fā)現(xiàn)按上面找到的菜單項是灰色的(不可點擊的),點擊下方Build Phases界面的白色區(qū)域來獲取Xcode的應(yīng)用焦點,然后重新試一下。
把RWUIControls.h從項目導航欄中拖到中央面板的Copy Headers下的Public部分。這一步確保任何使用你的庫的用戶均可以獲取該頭文件。
Note:顯然,所有包含在你的公共頭文件中的頭文件必須是對外公開的,這一點非常重要。否則,開發(fā)者在使用你的庫時會得到編譯錯誤。如果Xcode在讀取公共頭文件時不能讀到你忘記設(shè)為public的頭文件,這實在是太令人沮喪了。
創(chuàng)建一個UI控件
既然你已經(jīng)設(shè)置好你的工程了,是時候為你的庫添加一些功能了。由于本篇教程的關(guān)鍵在于教你怎么樣創(chuàng)建一個framework,而不是怎么樣構(gòu)建一個UI控件,這里你將使用上一篇教程中創(chuàng)建好的控件。在你之前下載好的壓縮包文件中找到RWKnobControl目錄,從Finder中拖到Xcode下RWUIControls目錄下。
選擇Copy items into destination group’s folder,點擊下方的選擇框,確保RWUIControls靜態(tài)庫目標被選中。
這一步默認把實現(xiàn)文件添加到編譯列表,把頭文件添加到Project組。這意味著它們目前是私有的。
Note:在你弄清楚之前,這三個組的名稱可能會讓你迷惑,Public是你期望的,Private下的頭文件依然是可以暴露出來的,因此名字可能有些誤導。諷刺的是,在Project下的頭文件對你的工程來說才是“私有”的,因此,你將會更多地希望你的頭文件或者在Public下,或者在Project下。
現(xiàn)在,你需要將控件的頭文件RWKnobControl.h分享出來,有幾種方式可以實現(xiàn)這一點,首先是在Copy Headers面板中將這個頭文件從Project欄拖到Public欄。
或者,你可能會發(fā)現(xiàn),更簡單的方法是,編輯文件,改變Target Membership面板下的membership。這個選項更方便一些,可以讓你不斷添加文件,擴充你的庫。
Note:如果你不斷往庫中添加新的類,記得及時更新這些類的關(guān)系(membership),使盡可能少的類成為public,并確保其他非public的頭文件都在Project下。
對你的控件的頭文件需要做的另一件事是將其添加到庫的主頭文件RWControls.h中。在這個主頭文件的幫助下,開發(fā)者使用你的庫僅僅需要導入一個頭文件,如下面的代碼一樣,而不是自己去選擇自己需要的一塊導入。
#import < RWUIControls/RWUIControls.h>
因此,在RWUIControls.h中添加下面的代碼:
// Knob Control
#import
配置Build Settings
現(xiàn)在距離構(gòu)建這個項目、創(chuàng)建靜態(tài)庫已經(jīng)非常接近了。不過,這里要先進行一些配置,讓我們的庫對于用戶來說更友好。
首先,你需要提供一個目錄名,表示你將把拷貝的公共頭文件存放到哪里。這樣確保當你使用靜態(tài)庫的時候可以定位到相關(guān)頭文件的位置。
在項目導航欄中點擊項目名,然后選擇RWUIControls靜態(tài)庫目標,選擇Build Setting欄,然后搜索public header,雙擊Public Headers Folder Path,在彈出視圖中鍵入如圖所示內(nèi)容:
一會你就會看到這個目錄了。
現(xiàn)在你需要改變一些其他的設(shè)置,尤其是那些在二進制庫中遺留下的設(shè)置,編譯器提供給你一個選項,來消除無效代碼:永遠不會被執(zhí)行的代碼。當然你也可以移除掉一些debug用符號,例如某些函數(shù)名稱或者其他跟debug相關(guān)的細節(jié)。
因為你正在創(chuàng)建framework供他人使用,最好禁掉這些功能(無效代碼和debug用符號),讓用戶自己選擇對自己的項目有利的部分使用。和之前一樣,使用搜索框,改變下述設(shè)置:
Dead Code Stripping設(shè)置為NO
Strip Debug Symbol During Copy 全部設(shè)置為NO
Strip Style設(shè)置為Non-Global Symbols
編譯然后運行,到目前為止沒什么可看的,不過確保項目可以成功構(gòu)建,沒有錯誤和警報是非常好的。
選擇目標為iOS Device,按下command + B進行編譯,一旦成功,工程導航欄中Product目錄下libRWUIControls.a文件將從紅色變?yōu)楹谏砻鳜F(xiàn)在該文件已經(jīng)存在了。右鍵單擊libRWUIControls.a,選擇Show in Finder。
ios_framework_successful_first_build-700x454.png
再此目錄下,你將看到靜態(tài)庫,libRWUIControls.a,以及其他你為頭文件指定的目錄。注意到,正如你所期望的,那些定為public的頭文件可以在此看到。
創(chuàng)建一個依賴開發(fā)(Dependent Development)工程
在無法看到真實效果的情況下為iOS開發(fā)一個UI控件庫是極其困難的,而這是我們現(xiàn)在面臨的問題。
沒有人期望你閉著眼睛開發(fā)出一個UI控件,因此在這一部分你將創(chuàng)建一個新的Xcode工程,該工程依賴于你剛剛創(chuàng)建好的庫。這意味著允許你使用示例app創(chuàng)建一個framework。當然,這部分代碼將和庫本身完全分離,結(jié)構(gòu)會非常清晰。
選擇File\Close Project關(guān)閉之前的靜態(tài)庫工程,使用File\New\Project創(chuàng)建一個新的工程,選擇iOS\Application\Single View Application,將新工程命名為UIControlDevApp,將類前綴命名為RW,選擇該工程只支持iPhone,最后將項目保存到和之前的RWUIControls相同的目錄下。
添加RWUIControls依賴庫,將RWUIControls.xcodeproj從Finder中拖到Xcode中UIControlDevApp組下。
ios_framework_import_library_into_dev_app-700x357.png
現(xiàn)在你可以在你的工程中導航到庫工程了,這樣做非常好,因為這樣意味著你可以在庫中編輯代碼,并且運行示例工程來測試你做的改變。
Note:你無法將同一工程在兩個Xcode窗口中同時打開,如果你發(fā)現(xiàn)你無法在你的工程中導航到庫工程的話,檢查一下是否庫工程在其他Xcode窗口中打開了。
這里你可以拷貝代碼,而不是和上一個教程似的重新創(chuàng)建代碼。首先,選擇Main.storyboard, RWViewController.h 和 RWViewController.m,然后右鍵單擊,選擇Delete,將它們刪除到廢紙簍中。然后,將你之前下載的壓縮文件中DevApp文件夾拷貝到Xcode的UIControlDevApp組下。
現(xiàn)在,你將添加靜態(tài)庫作為實例項目的依賴庫:
在項目導航欄中選擇UIControlDevApp。
導航到UIControlDevApp目標下Build Phases面板下。
打開Target Dependencies面板,點擊+按鈕調(diào)出選擇器。
找到RWUIControls靜態(tài)庫,選擇并點擊Add。這一步表明當構(gòu)建dev應(yīng)用時,Xcode會檢查是否靜態(tài)庫需要重新構(gòu)建。
為了連接到靜態(tài)庫本身,展開Link Binary With Libraries面板,再次點擊+按鈕,從Workspace組中選擇libRWUIControls.a然后點擊Add。
這一步確保Xcode可以連接到靜態(tài)庫,就像連接到系統(tǒng)framework(例如UIKit)一樣。
ios_framework_add_dependencies_to_dev_app.gif
編譯并運行,如果你按照之前的教程創(chuàng)建了一個旋鈕控件,在你眼前展示的將是與之相同的應(yīng)用。
像這樣使用嵌套工程的好處是你可以對庫本身做出修改,而不用離開示例工程,即使你同時改變兩個地方的代碼也一樣。每次你編譯工程,你都要檢查是否將頭文件的public/project關(guān)系設(shè)置正確。如果實例工程中缺失了任何需要的頭文件,它都不能被編譯。
創(chuàng)建一個Framework
到現(xiàn)在,你可能迫不及待地點著腳趾頭,想著什么時候framework可以出來。可以理解,因為到現(xiàn)在為止你已經(jīng)做了許多工作,然而卻沒有看到過framework的身影。
現(xiàn)在該有所改變了,你之所以到現(xiàn)在都沒有創(chuàng)建一個framework,是因為framework本身就是靜態(tài)庫加上一組頭文件——實際上正是你已經(jīng)創(chuàng)建好的東西。
當然,framework也有幾點不同之處:
目錄結(jié)構(gòu)。Framework有一個能被Xcode識別的特殊的目錄結(jié)構(gòu),你將會創(chuàng)建一個build task,由它來為你創(chuàng)建這種結(jié)構(gòu)。
片段(Slice)。目前為止,當你構(gòu)建庫時,僅僅考慮到當前需要的結(jié)構(gòu)(architecture)。例如,i386、arm7等,為了讓一個framework更有用,對于每一個運行framework的結(jié)構(gòu),該framework都需要構(gòu)建這種結(jié)構(gòu)。一會你就會創(chuàng)建一個新的工程,構(gòu)建所有需要的結(jié)構(gòu),并將它們包含到framework中。
這一部分非常神奇,不過我們會慢慢地來。實際上它并不像看起來那樣復雜。
Framework結(jié)構(gòu)
正如之前提到的,一個framework有一個特殊的目錄結(jié)構(gòu),看起來像是這樣的:
ios_framework_directory_structure-449x320.png
現(xiàn)在你需要在靜態(tài)庫構(gòu)建過程中添加腳本來創(chuàng)建這種結(jié)構(gòu),在項目導航欄中選擇RWUIControls,然后選擇RWUIControls靜態(tài)庫目標,選擇Build Phases欄,然后選擇Editor/Add Build Phase/Add Run Script Build Phase來添加一個新的腳本。
ios_framework_framework_add_run_script_build_phase-700x271.png
這一步在build phases部分添加了一個新的面板,這允許你在構(gòu)建時運行一個Bash腳本。你希望讓腳本在build的過程中何時執(zhí)行,就把這個面板拖動到列表中相對應(yīng)的那一位置。對于該framework工程來說,腳本最后執(zhí)行,因此你可以讓它保留在默認的位置即可。
雙擊面板標題欄Run Script,重命名為Build Framework。
在腳本文本框中粘貼下面的Bash腳本代碼
set -e
export FRAMEWORK_LOCN="${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.framework"
# Create the path to the real Headers die
mkdir -p "${FRAMEWORK_LOCN}/Versions/A/Headers"
# Create the required symlinks
/bin/ln -sfh A "${FRAMEWORK_LOCN}/Versions/Current"
/bin/ln -sfh Versions/Current/Headers "${FRAMEWORK_LOCN}/Headers"
/bin/ln -sfh "Versions/Current/${PRODUCT_NAME}" \
"${FRAMEWORK_LOCN}/${PRODUCT_NAME}"
# Copy the public headers into the framework
/bin/cp -a "${TARGET_BUILD_DIR}/${PUBLIC_HEADERS_FOLDER_PATH}/" \
"${FRAMEWORK_LOCN}/Versions/A/Headers"
這個腳本首先創(chuàng)建了RWUIControls.framework/Versions/A/Headers目錄,然后創(chuàng)建了一個framework所需要的三個連接符號(symbolic links)。
Versions/Current => A
Headers => Versions/Current/Headers
RWUIControls => Versions/Current/RWUIControls
最后,將公共頭文件從你之前定義的公共頭文件路徑拷貝到Versions/A/Headers目錄下,-a參數(shù)確保修飾次數(shù)作為拷貝的一部分不會改變,防止不必要的重新編譯。
現(xiàn)在,選擇RWUIControls靜態(tài)庫scheme,然后選擇iOS Device構(gòu)建目標,然后使用cmd+B構(gòu)建。
ios_framework_build_target_static_lib.png
在RWUIControls工程里Products目錄下右鍵單擊libRWUIControls.a靜態(tài)庫,然后再一次選擇Show in Finder。
ios_framework_static_lib_view_in_finder-480x295.png
在這次構(gòu)建目錄中你可以看到RWUIControls.framework,可以確定一下這里展示了正確的目錄結(jié)構(gòu):
ios_framework_created_framework_directory_structure-480x251.png
這算是在完成你的framework的過程中邁出了一大步。不過你會注意到這里并沒有一個靜態(tài)lib文件。這就是我們下一步將要解決的問題。
多架構(gòu)(Multi-Architecture)編譯
iOS app需要在許多不同的CPU架構(gòu)下運行:
arm7: 在最老的支持iOS7的設(shè)備上使用
arm7s: 在iPhone5和5C上使用
arm64: 運行于iPhone5S的64位 ARM 處理器 上
i386: 32位模擬器上使用
x86_64: 64為模擬器上使用
每個CPU架構(gòu)都需要不同的二進制數(shù)據(jù),當你編譯一個應(yīng)用時,無論你目前正在使用那種架構(gòu),Xcode都會正確地依照對應(yīng)的架構(gòu)編譯。例如,如果你想跑在虛擬機上,Xcode只會編譯i386版本(或者是64位機的x86_64版本)。
這意味著編譯會盡可能快地進行,當你歸檔一款app或者構(gòu)建app的發(fā)布版本(release mode)時,Xcode會構(gòu)建上述三個用于真機的ARM架構(gòu)。因此這樣app就可以跑在所有設(shè)備上了。不過,其他的編譯架構(gòu)又如何呢?
當你創(chuàng)建你的framework時,你自然會想讓所有開發(fā)者都能在所有可能的架構(gòu)上運行它,不是嗎?你當然想,因為這樣可以從同行那兒得到尊敬與贊美。
因此你需要讓Xcode在所有架構(gòu)下都進行編譯。這一過程實際上是創(chuàng)建了二進制FAT(File Allocation Table,文件配置表),它包含了所有架構(gòu)的片段(slice)。
Note:這里實際上強調(diào)了創(chuàng)建依賴靜態(tài)庫的示例項目的另一個原因:庫僅僅在示例項目運行所需要的架構(gòu)下編譯,只有當有變化的時候才重新編譯,為什么這一點會讓人激動?因為開發(fā)周期會盡可能地縮短。
這里將使用在RWUIControls工程中的一個新的目標來構(gòu)建framework,在項目導航欄中選擇RWUIControls,然后點擊已經(jīng)存在的目標下面的Add Target按鈕。
ios_framework_add_target_button-471x320.png
找到iOS/Other/Aggregate,點擊Next,將目標命名為Framework。
Note:為什么使用集合(Aggregate)目標來創(chuàng)建一個framework呢?為什么這么不直接?因為OS X對庫的支持更好一些,事實上,Xcode直接為每一個OS X工程提供一個Cocoa Framework編譯目標。基于此,你將使用集合編譯目標,作為Bash腳本的連接串來創(chuàng)建神奇的framework目錄結(jié)構(gòu)。你是不是開始覺得這里的方法有些愚蠢了?
為了確保每當這個新的framework目標被創(chuàng)建時,靜態(tài)鏈接庫都會被編譯,你需要往靜態(tài)庫目標中添加依賴(Dependency)。在庫工程中選擇Framework目標,在Build Phases中添加一個依賴。展開Target Dependencies面板,點擊 + 按鈕選擇RWUIControls靜態(tài)庫。
ios_framework_add_dependency_to_framework_target.gif
這個目標的主要編譯部分是多平臺編譯,你將使用一個腳本來做到這一點。和你之前做的一樣,在Framework目標下,選擇Build Phases欄,點擊Editor/Add Build Phase/Add Run Script Build Phase,創(chuàng)建一個新的Run Script Build Phase。
ios_framework_framework_add_run_script_build_phase-700x271.png
雙擊Run Script,重命名腳本的名字。這次命名為MultiPlatform Build。
在腳本文本框中粘貼下面的Bash腳本代碼:
set -e
# If we're already inside this script then die
if [ -n "$RW_MULTIPLATFORM_BUILD_IN_PROGRESS" ]; then
exit 0
fi
export RW_MULTIPLATFORM_BUILD_IN_PROGRESS=1
RW_FRAMEWORK_NAME=${PROJECT_NAME}
RW_INPUT_STATIC_LIB="lib${PROJECT_NAME}.a"
RW_FRAMEWORK_LOCATION="${BUILT_PRODUCTS_DIR}/${RW_FRAMEWORK_NAME}.framework"
set –e確保腳本的任何地方執(zhí)行失敗,則整個腳本都執(zhí)行失敗。這樣可以避免讓你創(chuàng)建一個部分有效的framework。
接著,用RW_MULTIPLATFORM_BUILD_IN_PROGRESS變量決定是否循環(huán)調(diào)用腳本,如果有該變量,則退出。
然后設(shè)定一些變量。該framework的名字與項目的名字一樣。也就是RWUIControls,另外,靜態(tài)lib的名字是libRWUIControls.a。
接下來,用腳本設(shè)置一些函數(shù),這些函數(shù)一會項目就會用到,把下面的代碼加到腳本的底部。
function build_static_library {
# Will rebuild the static library as specified
#? ? build_static_library sdk
xcrun xcodebuild -project "${PROJECT_FILE_PATH}" \
-target "${TARGET_NAME}" \
-configuration "${CONFIGURATION}" \
-sdk "${1}" \
ONLY_ACTIVE_ARCH=NO \
BUILD_DIR="${BUILD_DIR}" \
OBJROOT="${OBJROOT}" \
BUILD_ROOT="${BUILD_ROOT}" \
SYMROOT="${SYMROOT}" $ACTION
}
function make_fat_library {
# Will smash 2 static libs together
#? ? make_fat_library in1 in2 out
xcrun lipo -create "${1}" "${2}" -output "${3}"
}
build_static_library把SDK作為參數(shù),例如iPhone7.0,然后創(chuàng)建靜態(tài)lib,大多數(shù)參數(shù)直接傳到當前的構(gòu)建工作中來,不同的是設(shè)置ONLY_ACTIVE_ARCH來確保為當前SDK構(gòu)建所有的結(jié)構(gòu)。
make_fat_library使用lipo將兩個靜態(tài)庫合并為一個,其參數(shù)為兩個靜態(tài)庫和結(jié)果的輸出位置。從這里了解更多關(guān)于lipo的知識。
為了使用這兩個方法,接下來腳本將定義更多你要用到的變量,你需要知道其他SDK是什么,例如,iphoneos7.0應(yīng)該對應(yīng)iphonesimulator7.0,反過來也一樣。你也需要找到該SDK對應(yīng)的編譯目錄。
把下面的代碼添加到腳本的底部。
# 1 - Extract the platform (iphoneos/iphonesimulator) from the SDK name
if [[ "$SDK_NAME" =~ ([A-Za-z]+) ]]; then
RW_SDK_PLATFORM=${BASH_REMATCH[1]}
else
echo "Could not find platform name from SDK_NAME: $SDK_NAME"
exit 1
fi
# 2 - Extract the version from the SDK
if [[ "$SDK_NAME" =~ ([0-9]+.*$) ]]; then
RW_SDK_VERSION=${BASH_REMATCH[1]}
else
echo "Could not find sdk version from SDK_NAME: $SDK_NAME"
exit 1
fi
# 3 - Determine the other platform
if [ "$RW_SDK_PLATFORM" == "iphoneos" ]; then
RW_OTHER_PLATFORM=iphonesimulator
else
RW_OTHER_PLATFORM=iphoneos
fi
# 4 - Find the build directory
if [[ "$BUILT_PRODUCTS_DIR" =~ (.*)$RW_SDK_PLATFORM$ ]]; then
RW_OTHER_BUILT_PRODUCTS_DIR="${BASH_REMATCH[1]}${RW_OTHER_PLATFORM}"
else
echo "Could not find other platform build directory."
exit 1
fi
上面四句聲明都非常相似,都是使用字符串比較和正則表達式來確定RW_OTHER_PLATFORM和RW_OTHER_BUILT_PRODUCTS_DIR。
詳細解釋一下上面四句聲明:
SDK_NAME將指代iphoneos7.0和iphonesimulator6.1,這個正則表達式取出字符串開頭不包含數(shù)字的那些字符,因此,其結(jié)果是iphoneos 或 iphonesimulator。
這個正則表達式取出SDK_NAME中表示版本用的數(shù)字,7.0或6.1等。
這里用簡單的字符串比較來將iphonesimulator 轉(zhuǎn)換為iphoneos,反過來也一樣。
從構(gòu)建好的工程的目錄路徑的末尾找出平臺名稱,將其替換為其他平臺。這樣可以確保為其他平臺構(gòu)建的目錄可以被找到。這是將兩個靜態(tài)庫合并的關(guān)鍵部分。
現(xiàn)在你可以啟動腳本為其他平臺編譯,然后得到合并兩靜態(tài)庫的結(jié)果。
在腳本最后添加下面的代碼:
# Build the other platform.
build_static_library "${RW_OTHER_PLATFORM}${RW_SDK_VERSION}"
# If we're currently building for iphonesimulator, then need to rebuild
#? to ensure that we get both i386 and x86_64
if [ "$RW_SDK_PLATFORM" == "iphonesimulator" ]; then
build_static_library "${SDK_NAME}"
fi
# Join the 2 static libs into 1 and push into the .framework
make_fat_library "${BUILT_PRODUCTS_DIR}/${RW_INPUT_STATIC_LIB}" \
"${RW_OTHER_BUILT_PRODUCTS_DIR}/${RW_INPUT_STATIC_LIB}" \
"${RW_FRAMEWORK_LOCATION}/Versions/A/${RW_FRAMEWORK_NAME}"
首先,調(diào)用你之前定義好的函數(shù)為其他平臺編譯
如果你現(xiàn)在正在為模擬器編譯,那么Xcode會默認只在該系統(tǒng)對應(yīng)的結(jié)構(gòu)下編譯,例如i386 或 x86_64。為了在這兩個結(jié)構(gòu)下都進行編譯,這里調(diào)用了build_static_library,基于iphonesimulator SDK重新編譯,確保這兩個結(jié)構(gòu)都進行了編譯。
最后調(diào)用make_fat_library將在當前編譯目錄下的靜態(tài)lib同在其他目錄下地lib合并,依次實現(xiàn)支持多結(jié)構(gòu)的FAT靜態(tài)庫。這個被放到了framework中。
腳本的最后是簡單的拷貝命令,將下面代碼添加到腳本最后:
# Ensure that the framework is present in both platform's build directories
cp -a "${RW_FRAMEWORK_LOCATION}/Versions/A/${RW_FRAMEWORK_NAME}" \
"${RW_OTHER_BUILT_PRODUCTS_DIR}/${RW_FRAMEWORK_NAME}.framework/Versions/A/${RW_FRAMEWORK_NAME}"
# Copy the framework to the user's desktop
ditto "${RW_FRAMEWORK_LOCATION}" "${HOME}/Desktop/${RW_FRAMEWORK_NAME}.framework"
第一條命令確保framework在所有平臺的編譯目錄中都存在。
第二條將完成的framework拷貝到用戶的桌面上,這一步是可選的,但我發(fā)現(xiàn)這樣做可以很方便的存取framework。
選擇Framework集合方案(aggregate scheme),按下cmd+B編譯該framework。
ios_framework_select_framework_aggregate_scheme-480x135.png
這一步將構(gòu)建并在你的桌面上存放一個RWUIControls.framework。
ios_framework_built_framework_on_desktop-700x319.png
為了檢查一下我們的多平臺編譯真的成功了,啟動終端,導航到桌面上的framework,像下面一樣:
$ cd ~/Desktop/RWUIControls.framework
$ RWUIControls.framework? xcrun lipo -info RWUIControls
第一條指令導航到framework中,第二行使用lipo指令從RWUIControls靜態(tài)庫中得到需要的信息,這將列出存在于該庫中的所有片段。
ios_framework_architectures_in_fat_library-700x257.png
這里你可以看到,一共有五種片段:i386, x86_64, arm7, arm7s 和 arm64,正如你在編譯時設(shè)定的那樣。如果你之前使用lipo –info指令,你可以看到這些片段的一個分組。
如何使用Framework?
OK,你已經(jīng)有了framework,你也有了庫。它們可以提供一種優(yōu)雅的方法來解決你迄今為止還沒有遇到過的問題,但是做這些的意義是什么呢?
使用framework的其中一個主要的優(yōu)點是簡化使用,現(xiàn)在你將創(chuàng)建一個簡單的iOS應(yīng)用,并使用你剛剛創(chuàng)建好的RWUIControls.framework。
使用Xcode創(chuàng)建一個新工程,選擇File/New/Project,然后選擇iOS/Application/Single View Application,將新工程命名為ImageViewer,設(shè)置為僅僅用于iPhone,將其保存到與之前兩個工程同樣的目錄下。這個應(yīng)用將展示一張圖片,允許用戶使用RWKnobControl旋轉(zhuǎn)圖片。
在你之前下載的壓縮文件中找到ImageViewer目錄,這里面只有一個圖片文件,把這個圖片文件sampleImage.jpg從Finder中拖到Xcode的ImageViewer組中。
ios_framework_drag_sample_image_into_xcode-480x299.png
選中Copy items into destination group’s folder,點擊Finish完成導入操作。
導入一個framework的步驟幾乎相同,將RWUIControls.framework從桌面拖到Xcode中的Frameworks組下。同樣,確保選中了Copy items into destination group’s folder。
ios_framework_import_framework.gif
打開RWViewController.m,將里面的代碼替換為下面的代碼:
#import "RWViewController.h"
#import < RWUIControls/RWUIControls.h>
@interface RWViewController ()
@property (nonatomic, strong) UIImageView *imageView;
@property (nonatomic, strong) RWKnobControl *rotationKnob;
@end
@implementation RWViewController
- (void)viewDidLoad
{
[super viewDidLoad];
// Create UIImageView
CGRect frame = self.view.bounds;
frame.size.height *= 2/3.0;
self.imageView = [[UIImageView alloc] initWithFrame:CGRectInset(frame, 0, 20)];
self.imageView.image = [UIImage imageNamed:@"sampleImage.jpg"];
self.imageView.contentMode = UIViewContentModeScaleAspectFit;
[self.view addSubview:self.imageView];
// Create RWKnobControl
frame.origin.y += frame.size.height;
frame.size.height /= 2;
frame.size.width? = frame.size.height;
self.rotationKnob = [[RWKnobControl alloc] initWithFrame:CGRectInset(frame, 10, 10)];
CGPoint center = self.rotationKnob.center;
center.x = CGRectGetMidX(self.view.bounds);
self.rotationKnob.center = center;
[self.view addSubview:self.rotationKnob];
// Set up config on RWKnobControl
self.rotationKnob.minimumValue = -M_PI_4;
self.rotationKnob.maximumValue = M_PI_4;
[self.rotationKnob addTarget:self
action:@selector(rotationAngleChanged:)
forControlEvents:UIControlEventValueChanged];
}
- (void)rotationAngleChanged:(id)sender
{
self.imageView.transform = CGAffineTransformMakeRotation(self.rotationKnob.value);
}
- (NSUInteger)supportedInterfaceOrientations
{
return UIInterfaceOrientationMaskPortrait;
}
@end
這就是一個簡單的視圖控制器,它做了以下幾件事:
使用#import導入框架的頭文件
設(shè)置了一組私有屬性來持有UIImageView和RWKnobControl。
創(chuàng)建一個UIImageView,將其放到合適的位置。
為Knob control設(shè)置一些屬性,包括添加值改變的事件監(jiān)聽器。相應(yīng)方法為rotationAngleChanged:方法。
rotationAngleChanged:方法簡單更新了UIImageView的transform屬性,讓圖片隨著knob control的移動而旋轉(zhuǎn)。
具體怎么樣使用RWKnobControl,可以看一下上一篇教程,那里解釋了怎么樣去創(chuàng)建它。
編譯并運行,你就能看到一款簡單的應(yīng)用,當你改變knob control的值時圖片就會旋轉(zhuǎn)。
ios_framework_image_viewer_rotating.gif
打包(Bundle)資源
你有沒有注意到RWUIControls的framework只包含了代碼和頭文件,其他的文件卻沒有被包含。例如,你沒有使用其他任何資源,比如圖片。這是iOS的一個限制,framework只能包含頭文件和靜態(tài)庫。
現(xiàn)在準備好,這篇教程要開始進階了。這一部分你將學到怎么樣通過使用bundle整合資源,讓其可以隨著framework一起發(fā)布,進而突破這一限制。
你將創(chuàng)建一個新的UI控件——絲帶控件,作為RWUIControls庫的一部分。這個控件將在一個UIView的右上方展示一個絲帶圖片。
創(chuàng)建一個Bundle
資源都會被添加到bundle中,這將是RWUIControls工程上的另一個目標。
打開UIControlDevApp工程,選擇RWUIControls子工程,點擊Add Target按鈕,導航到OS X/Framework and Library/Bundle。將新的Bundle命名為RWUIControlsResources,然后從framework選擇框中選擇Core Foundation。
ios_framework_import_framework.gif
這里需要配置幾個編譯設(shè)置,因為你正在創(chuàng)建一個在iOS上使用的bundle,這與默認的OS X不同。選擇RWUIControlsResources目標,然后點擊Build Settings欄,搜索base sdk,選擇Base SDK這一行,按下delete鍵,這一步將OS X切換為iOS。
ios_framework_bundle_set_base_sdk-700x161.png
同時你需要將工程名稱改為RWUIControls。搜索product name,雙擊進入編輯模式,將${TARGET_NAME}替換為RWUIControls。
ios_framework_bundle_set_product_name-700x206.png
默認情況下,有兩種resolutions的圖片可以產(chǎn)生一些有趣的現(xiàn)象。例如,當你導入一個retina @2x版本的圖片時,普通版的和Retina版的將會合并成一個多resolution的TIFF(標簽圖像文件格式,Tagged Image File Format)。這不是一件好事。搜索hidpi將COMBINE_HIDPI_IMAGES設(shè)置為NO。
ios_framework_bundle_hidpi_images-700x234.png
現(xiàn)在,你將確保當你編譯framework時,bundle也能被編譯并將framework作為依賴添加到集體目標中。選中Framework目標,選擇Build Phases欄,展開Target Dependencies面板,點擊 + 按鈕,選擇RWUIControlsResources目標將其添加為依賴。
ios_framework_add_bundle_as_framework_dependency.gif
現(xiàn)在,在Framework目標的Build Phases中,打開MultiPlatform Build面板,在腳本的最后添加下述代碼:
# Copy the resources bundle to the user's desktop
ditto "${BUILT_PRODUCTS_DIR}/${RW_FRAMEWORK_NAME}.bundle" \
"${HOME}/Desktop/${RW_FRAMEWORK_NAME}.bundle"
這條指令將拷貝構(gòu)建好的bundle到用戶的桌面上。現(xiàn)在,編譯framework scheme,你會發(fā)現(xiàn)bundle在桌面上出現(xiàn)。
ios_framework_bundle_on_desktop-198x320.png
導入Bundle
為了用這個新的bundle開發(fā),你需要在示例項目中使用它,這意味著你必須既把它作為依賴添加到工程中,同時作為一個對象拷貝到項目中。
在項目導航欄中,選擇UIControlDevApp工程,點擊UIControlDevApp目標,展開RWUIControls工程的Product組,把RWUIControls.bundle拖到Copy Bundle Resources面板中的 Build Phases欄。
在Target Dependencies面板中,點擊+按鈕,添加新的依賴,然后選擇RWUIControlsResources。
ios_framework_add_bundle_for_dev_project.gif
創(chuàng)建一個絲帶視圖(Ribbon View)
上面的就是所有必需的配置工作了,從你之前下載的壓縮文件中將RWRibbon文件夾拖入到RWUIControls工程下RWUIControls組中。
ios_framework_drag_rwribbon-480x309.png
選中Copy the items into the destination group’s folder,在對應(yīng)的選擇框中打勾,確保它被添加到RWUIControls靜態(tài)lib目標中。
ios_framework_rwribbon_membership-700x472.png
代碼中一個很重要的部分是你怎樣引用一張圖片。如果你看一下RWRibbonView.m文件中的addRibbonView方法,你將會看到相關(guān)的這一行代碼:
UIImage *image = [UIImage imageNamed:@"RWUIControls.bundle/RWRibbon"];
Bundle就像一個文件目錄,所以引用bundle中的一張圖片是非常簡單的。
將圖片添加到bundle中,選擇這張圖片,在右邊的面板中,通過選擇來表示它應(yīng)該屬于RWUIControlsResources目標。
ios_framework_rwribbon_image_membership-700x208.png
還記得我們說過要確保framework可以被訪問嗎?現(xiàn)在,你需要導出頭文件RWRibbon.h,在Target Membership面板中選擇該文件,然后從彈出視圖中選擇Public。
ios_framework_rwribbon_header_membership-480x262.png
最后,你需要將頭文件引用添加到framework的頭文件中。打開RWUIControls.h添加下面這兩行:
// RWRibbon
#import < RWUIControls/RWRibbonView.h>
將絲帶添加到示例工程中
在UIControlDevApp項目中打開RWViewController.m文件,在@interface后的大括號中添加下面的實例變量聲明。
RWRibbonView? *_ribbonView;
在viewDidLoad:的末尾添加下面的代碼來創(chuàng)建一個絲帶視圖:
// Creates a sample ribbon view
_ribbonView = [[RWRibbonView alloc] initWithFrame:self.ribbonViewContainer.bounds];
[self.ribbonViewContainer addSubview:_ribbonView];
// Need to check that it actually works :)
UIView *sampleView = [[UIView alloc] initWithFrame:_ribbonView.bounds];
sampleView.backgroundColor = [UIColor lightGrayColor];
[_ribbonView addSubview:sampleView];
編譯并運行UIControlDevApp scheme。你將看到新的絲帶控件出現(xiàn)在應(yīng)用的下方。
ios_framework_dev_app_with_ribbon-333x500.png
在ImageViewer中使用Bundle
我要向你分享的最后一件事是怎么樣在其他應(yīng)用中使用這個新的bundle,例如,你之前創(chuàng)建的ImageViewer應(yīng)用。
開始之前,確保你的bundle和framework都是最新版本的,選擇Framework scheme然后按下cmd+B編譯。
打開ImageViewer工程,找到Frameworks組中的RWUIControls.framework項目,然后將其刪除,選擇Move to Trash。然后將RWUIControls.framework從你的桌面上拖到Frameworks組中。這是必須的,因為此時的framework已經(jīng)與你第一次導入時的framework大不相同了。
ios_framework_delete_framework-700x283.png
Note:如果Xcode拒絕讓你添加framework,這可能是因為你并沒有真正將之前版本的framework刪除到廢紙簍。如果是因為這樣的話,從Finder中ImageViewer目錄下刪除framework然后重新嘗試。
導入bundle,簡單將其從桌面上拖到ImageViewer組中。選中Copy items into destination group’s folder,選中對應(yīng)的選擇框,確保它被添加到ImageViewer目標中。
ios_framework_import_bundle-700x474.png
接下來你要將絲帶添加到可以旋轉(zhuǎn)的圖片上。因此,在RWViewController.m文件中代碼要有一些簡單的變動。
打開該文件,將屬性imageView的類型從UIImageView變?yōu)镽WRibbonView:
@property (nonatomic, strong) RWRibbonView *imageView;
將viewDidLoad方法中第一部分,負責創(chuàng)建并配置UIImageView的代碼,替換為下面的代碼:
[super viewDidLoad];
// Create UIImageView
CGRect frame = self.view.bounds;
frame.size.height *= 2/3.0;
self.imageView = [[RWRibbonView alloc] initWithFrame:CGRectInset(frame, 0, 20)];
UIImageView *iv = [[UIImageView alloc] initWithFrame:self.imageView.bounds];
iv.image = [UIImage imageNamed:@"sampleImage.jpg"];
iv.contentMode = UIViewContentModeScaleAspectFit;
[self.imageView addSubview:iv];
[self.view addSubview:self.imageView];
編譯并運行該項目,現(xiàn)在該項目中你同時使用了RWUIControls framework下的RWKnobControl和RWRibbonView。
ios_framework_image_viewer_with_ribbon.gif