(原文:How to Create a Framework for iOS 作者:Sam Davies 譯者:Mr_cyz )
在上一篇教程(中譯版)中,你學到了怎麼樣創建一個可復用的圓形旋鈕控件。然而你可能不清楚怎樣讓其他開發者更方便地去復用它。
如果你想將你開發的控件與別人分享,一種方法是直接提供源代碼文件。然而,這種方法並不是很優雅。它會暴露所有的實現細節,而這些實現你可能並不想開源出來。此外,開發者也可能並不想看到你的所有代碼,因為他們可能僅僅希望將你的這份漂亮代碼的一部分植入自己的應用中。
另一種方法是將你的代碼編譯成靜態庫(library),讓其他開發者添加到自己的項目中。然而,這需要你一並公布所有的公開的頭文件,實在是非常不方便。
你需要一種簡單的方法來編譯你的代碼,這種方法應該使得你的代碼易分享,並且在多個工程中易復用。你需要的是一種方法來打包你的靜態庫,將所有的頭文件放到一個單元中,這樣你就可以立刻將其加入到你的項目中並使用。
非常幸運,這正是本篇教程所要解決的問題。你將會學到制作並使用Framework,幫助你解決這個頭疼的問題。OS X完美地支持這一點,因為Xcode就提供了一個項目模板,包含著默認構建目標(target)和可以容納類似於圖片、聲音、字體等資源的文件。你可以為iOS創建Framework,不過這是一個比較復雜的手工活,如果你跟著教程走,你將學到怎麼樣跨過路障,順利地完成Framework的創建。
當你跟著這篇教程走完後,你將能夠:
使用Xcode構建一個基本的靜態庫工程。
依賴於該靜態庫工程構建一款應用。
掌握如何將靜態庫工程轉換為完整的、合格的Framework。
最終,你將看到如何將一個圖像文件同Framework一起打包到resource bundle下。
這篇教程的主要目的是解釋怎麼樣在你的iOS工程中創建並使用一個Framework。所以,不像其他網站上的教程,這篇教程將只使用一小部分Objective-C代碼,並且這一小部分主要是為了說明我們將會遇到的一些概念。
從這裡下載可用的資源文件RWKnobControl。如果你在Creating a Static Library Project 這篇文章中完成了創建第一個項目的過程,這裡你將會看到怎麼樣使用去它們。
在創建本工程時,你將要創建的所有的代碼和項目文件都可以在Github上找到。對於本篇教程中每個創建階段都有不同的commit。
Framework是資源的集合,將靜態庫和其頭文件包含到一個結構中,讓Xcode可以方便地把它納入到你的項目中。
在OS X上,可能會創建一個動態連接(Dynamically Linked)的framework。通過動態連接,framework可以更新,不需要應用重新連接。在運行時,庫中代碼的一份拷貝被分享出來,整個工程都可以使用它,因此,這樣減少了內存消耗,提高了系統的性能。正如你看到的,這是一個功能強大的特性。
在iOS上,你不能用這種方式添加為系統添加自定義的framework,因此僅有的動態鏈接的framework只能是Apple提供的那些。(編者注:在iOS 8中已加入此特性,開發者可以使用第三方的動態框架)
然而,這並不意味著framework對於iOS而言是無關緊要的,靜態連接的framework依然可以打包代碼,使其在不同的應用中復用。
由於framework本質上是靜態庫的“一站式采購點”,因此在本篇教程中你所做的第一件事就是創建並使用靜態庫。當跟著教程走到如何創建framework時,你就能明白你所做的一切了,整體思路也不會那麼煙霧缭繞了。
打開Xcode,點擊File\New\Project,選擇iOS\Framework and Library\Cocoa Touch Static Library新建一個靜態庫工程.
將工程命名為RWUIControls,然後將工程保存到一個空目錄下。
一個靜態庫工程由頭文件和實現文件組成,這些文件將被編譯為庫本身。
為了方便其他開發者使用你的庫和framework,你將進行一些操作,讓他們僅需要導入一個頭文件便可以訪問所有你想公開的類。
當創建靜態庫工程時,Xcode會自動添加RWUIControls.h和RWUIControls.m。你不需要實現文件,因此右鍵單擊RWUIControls.m選擇delete,將它刪除到廢紙簍中。
打開RWUIControls.h,將所有內容替換為:
#import < UIKit/UIKit.h>
導入UIKit的頭文件,這是創建一個庫所需要的。當你在創建不同的組成類時,你將會將它們添加到這個文件中,確保它們能夠被庫的使用者獲取到。
你所構建的項目依賴於UIKit,然而Xcode的靜態庫工程不會自動連接到UIKit。要解決這個問題,就要將UIKit作為依賴庫添加到工程中。在工程導航欄中選擇工程名,然後在中央面板中選擇RWUIControls目標。
點擊BuildPhases,展開Link Binary with Libraries這一部分,點擊+添加一個新的framework,找到UIKit.framework,點擊add添加進來。
如果不結合頭文件,靜態庫是沒有用的,靜態庫編譯一組文件,在這些文件中類和方法都以二進制數據的形式存在。在你創建的庫中,有些類將能夠被公開訪問到,有些類只能由庫內部訪問並使用。
接下來,你需要在build欄中添加新的phase,來包含所有頭文件,並將它們放到編譯器可以獲取到的某個地方。然後,你將會拷貝這些到你的framework中。
依然是在Xcode的Build Phases界面,選擇Editor\Add Build Phase\Add Copy Headers Build Phase。
Note:如果你發現按上面找到的菜單項是灰色的(不可點擊的),點擊下方Build Phases界面的白色區域來獲取Xcode的應用焦點,然後重新試一下。
把RWUIControls.h從項目導航欄中拖到中央面板的Copy Headers下的Public部分。這一步確保任何使用你的庫的用戶均可以獲取該頭文件。
Note:顯然,所有包含在你的公共頭文件中的頭文件必須是對外公開的,這一點非常重要。否則,開發者在使用你的庫時會得到編譯錯誤。如果Xcode在讀取公共頭文件時不能讀到你忘記設為public的頭文件,這實在是太令人沮喪了。
既然你已經設置好你的工程了,是時候為你的庫添加一些功能了。由於本篇教程的關鍵在於教你怎麼樣創建一個framework,而不是怎麼樣構建一個UI控件,這裡你將使用上一篇教程中創建好的控件。在你之前下載好的壓縮包文件中找到RWKnobControl目錄,從Finder中拖到Xcode下RWUIControls目錄下。
選擇Copy items into destination group’s folder,點擊下方的選擇框,確保RWUIControls靜態庫目標被選中。
這一步默認把實現文件添加到編譯列表,把頭文件添加到Project組。這意味著它們目前是私有的。
Note:在你弄清楚之前,這三個組的名稱可能會讓你迷惑,Public是你期望的,Private下的頭文件依然是可以暴露出來的,因此名字可能有些誤導。諷刺的是,在Project下的頭文件對你的工程來說才是“私有”的,因此,你將會更多地希望你的頭文件或者在Public下,或者在Project下。
現在,你需要將控件的頭文件RWKnobControl.h分享出來,有幾種方式可以實現這一點,首先是在Copy Headers面板中將這個頭文件從Project欄拖到Public欄。
或者,你可能會發現,更簡單的方法是,編輯文件,改變Target Membership面板下的membership。這個選項更方便一些,可以讓你不斷添加文件,擴充你的庫。
Note:如果你不斷往庫中添加新的類,記得及時更新這些類的關系(membership),使盡可能少的類成為public,並確保其他非public的頭文件都在Project下。
對你的控件的頭文件需要做的另一件事是將其添加到庫的主頭文件RWControls.h中。在這個主頭文件的幫助下,開發者使用你的庫僅僅需要導入一個頭文件,如下面的代碼一樣,而不是自己去選擇自己需要的一塊導入。
#import < RWUIControls/RWUIControls.h>
因此,在RWUIControls.h中添加下面的代碼:
// Knob Control #import
現在距離構建這個項目、創建靜態庫已經非常接近了。不過,這裡要先進行一些配置,讓我們的庫對於用戶來說更友好。
首先,你需要提供一個目錄名,表示你將把拷貝的公共頭文件存放到哪裡。這樣確保當你使用靜態庫的時候可以定位到相關頭文件的位置。
在項目導航欄中點擊項目名,然後選擇RWUIControls靜態庫目標,選擇Build Setting欄,然後搜索public header,雙擊Public Headers Folder Path,在彈出視圖中鍵入如圖所示內容:
一會你就會看到這個目錄了。
現在你需要改變一些其他的設置,尤其是那些在二進制庫中遺留下的設置,編譯器提供給你一個選項,來消除無效代碼:永遠不會被執行的代碼。當然你也可以移除掉一些debug用符號,例如某些函數名稱或者其他跟debug相關的細節。
因為你正在創建framework供他人使用,最好禁掉這些功能(無效代碼和debug用符號),讓用戶自己選擇對自己的項目有利的部分使用。和之前一樣,使用搜索框,改變下述設置:
Dead Code Stripping設置為NO
Strip Debug Symbol During Copy 全部設置為NO
Strip Style設置為Non-Global Symbols
編譯然後運行,到目前為止沒什麼可看的,不過確保項目可以成功構建,沒有錯誤和警報是非常好的。
選擇目標為iOS Device,按下command + B進行編譯,一旦成功,工程導航欄中Product目錄下libRWUIControls.a文件將從紅色變為黑色,表明現在該文件已經存在了。右鍵單擊libRWUIControls.a,選擇Show in Finder。
再此目錄下,你將看到靜態庫,libRWUIControls.a,以及其他你為頭文件指定的目錄。注意到,正如你所期望的,那些定為public的頭文件可以在此看到。
在無法看到真實效果的情況下為iOS開發一個UI控件庫是極其困難的,而這是我們現在面臨的問題。
沒有人期望你閉著眼睛開發出一個UI控件,因此在這一部分你將創建一個新的Xcode工程,該工程依賴於你剛剛創建好的庫。這意味著允許你使用示例app創建一個framework。當然,這部分代碼將和庫本身完全分離,結構會非常清晰。
選擇File\Close Project關閉之前的靜態庫工程,使用File\New\Project創建一個新的工程,選擇iOS\Application\Single View Application,將新工程命名為UIControlDevApp,將類前綴命名為RW,選擇該工程只支持iPhone,最後將項目保存到和之前的RWUIControls相同的目錄下。
添加RWUIControls依賴庫,將RWUIControls.xcodeproj從Finder中拖到Xcode中UIControlDevApp組下。
現在你可以在你的工程中導航到庫工程了,這樣做非常好,因為這樣意味著你可以在庫中編輯代碼,並且運行示例工程來測試你做的改變。
Note:你無法將同一工程在兩個Xcode窗口中同時打開,如果你發現你無法在你的工程中導航到庫工程的話,檢查一下是否庫工程在其他Xcode窗口中打開了。
這裡你可以拷貝代碼,而不是和上一個教程似的重新創建代碼。首先,選擇Main.storyboard, RWViewController.h 和 RWViewController.m,然後右鍵單擊,選擇Delete,將它們刪除到廢紙簍中。然後,將你之前下載的壓縮文件中DevApp文件夾拷貝到Xcode的UIControlDevApp組下。
現在,你將添加靜態庫作為實例項目的依賴庫:
在項目導航欄中選擇UIControlDevApp。
導航到UIControlDevApp目標下Build Phases面板下。
打開Target Dependencies面板,點擊+按鈕調出選擇器。
找到RWUIControls靜態庫,選擇並點擊Add。這一步表明當構建dev應用時,Xcode會檢查是否靜態庫需要重新構建。
為了連接到靜態庫本身,展開Link Binary With Libraries面板,再次點擊+按鈕,從Workspace組中選擇libRWUIControls.a然後點擊Add。
這一步確保Xcode可以連接到靜態庫,就像連接到系統framework(例如UIKit)一樣。
編譯並運行,如果你按照之前的教程創建了一個旋鈕控件,在你眼前展示的將是與之相同的應用。
像這樣使用嵌套工程的好處是你可以對庫本身做出修改,而不用離開示例工程,即使你同時改變兩個地方的代碼也一樣。每次你編譯工程,你都要檢查是否將頭文件的public/project關系設置正確。如果實例工程中缺失了任何需要的頭文件,它都不能被編譯。
到現在,你可能迫不及待地點著腳趾頭,想著什麼時候framework可以出來。可以理解,因為到現在為止你已經做了許多工作,然而卻沒有看到過framework的身影。
現在該有所改變了,你之所以到現在都沒有創建一個framework,是因為framework本身就是靜態庫加上一組頭文件——實際上正是你已經創建好的東西。
當然,framework也有幾點不同之處:
目錄結構。Framework有一個能被Xcode識別的特殊的目錄結構,你將會創建一個build task,由它來為你創建這種結構。
片段(Slice)。目前為止,當你構建庫時,僅僅考慮到當前需要的結構(architecture)。例如,i386、arm7等,為了讓一個framework更有用,對於每一個運行framework的結構,該framework都需要構建這種結構。一會你就會創建一個新的工程,構建所有需要的結構,並將它們包含到framework中。
這一部分非常神奇,不過我們會慢慢地來。實際上它並不像看起來那樣復雜。
正如之前提到的,一個framework有一個特殊的目錄結構,看起來像是這樣的:
現在你需要在靜態庫構建過程中添加腳本來創建這種結構,在項目導航欄中選擇RWUIControls,然後選擇RWUIControls靜態庫目標,選擇Build Phases欄,然後選擇Editor/Add Build Phase/Add Run Script Build Phase來添加一個新的腳本。
這一步在build phases部分添加了一個新的面板,這允許你在構建時運行一個Bash腳本。你希望讓腳本在build的過程中何時執行,就把這個面板拖動到列表中相對應的那一位置。對於該framework工程來說,腳本最後執行,因此你可以讓它保留在默認的位置即可。
雙擊面板標題欄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"
這個腳本首先創建了RWUIControls.framework/Versions/A/Headers目錄,然後創建了一個framework所需要的三個連接符號(symbolic links)。
Versions/Current => A
Headers => Versions/Current/Headers
RWUIControls => Versions/Current/RWUIControls
最後,將公共頭文件從你之前定義的公共頭文件路徑拷貝到Versions/A/Headers目錄下,-a參數確保修飾次數作為拷貝的一部分不會改變,防止不必要的重新編譯。
現在,選擇RWUIControls靜態庫scheme,然後選擇iOS Device構建目標,然後使用cmd+B構建。
在RWUIControls工程裡Products目錄下右鍵單擊libRWUIControls.a靜態庫,然後再一次選擇Show in Finder。
在這次構建目錄中你可以看到RWUIControls.framework,可以確定一下這裡展示了正確的目錄結構:
這算是在完成你的framework的過程中邁出了一大步。不過你會注意到這裡並沒有一個靜態lib文件。這就是我們下一步將要解決的問題。
iOS app需要在許多不同的CPU架構下運行:
arm7: 在最老的支持iOS7的設備上使用
arm7s: 在iPhone5和5C上使用
arm64: 運行於iPhone5S的64位 ARM 處理器 上
i386: 32位模擬器上使用
x86_64: 64為模擬器上使用
每個CPU架構都需要不同的二進制數據,當你編譯一個應用時,無論你目前正在使用那種架構,Xcode都會正確地依照對應的架構編譯。例如,如果你想跑在虛擬機上,Xcode只會編譯i386版本(或者是64位機的x86_64版本)。
這意味著編譯會盡可能快地進行,當你歸檔一款app或者構建app的發布版本(release mode)時,Xcode會構建上述三個用於真機的ARM架構。因此這樣app就可以跑在所有設備上了。不過,其他的編譯架構又如何呢?
當你創建你的framework時,你自然會想讓所有開發者都能在所有可能的架構上運行它,不是嗎?你當然想,因為這樣可以從同行那兒得到尊敬與贊美。
因此你需要讓Xcode在所有架構下都進行編譯。這一過程實際上是創建了二進制FAT(File Allocation Table,文件配置表),它包含了所有架構的片段(slice)。
Note:這裡實際上強調了創建依賴靜態庫的示例項目的另一個原因:庫僅僅在示例項目運行所需要的架構下編譯,只有當有變化的時候才重新編譯,為什麼這一點會讓人激動?因為開發周期會盡可能地縮短。
這裡將使用在RWUIControls工程中的一個新的目標來構建framework,在項目導航欄中選擇RWUIControls,然後點擊已經存在的目標下面的Add Target按鈕。
找到iOS/Other/Aggregate,點擊Next,將目標命名為Framework。
Note:為什麼使用集合(Aggregate)目標來創建一個framework呢?為什麼這麼不直接?因為OS X對庫的支持更好一些,事實上,Xcode直接為每一個OS X工程提供一個Cocoa Framework編譯目標。基於此,你將使用集合編譯目標,作為Bash腳本的連接串來創建神奇的framework目錄結構。你是不是開始覺得這裡的方法有些愚蠢了?
為了確保每當這個新的framework目標被創建時,靜態鏈接庫都會被編譯,你需要往靜態庫目標中添加依賴(Dependency)。在庫工程中選擇Framework目標,在Build Phases中添加一個依賴。展開Target Dependencies面板,點擊 + 按鈕選擇RWUIControls靜態庫。
這個目標的主要編譯部分是多平台編譯,你將使用一個腳本來做到這一點。和你之前做的一樣,在Framework目標下,選擇Build Phases欄,點擊Editor/Add Build Phase/Add Run Script Build Phase,創建一個新的Run Script Build Phase。
雙擊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確保腳本的任何地方執行失敗,則整個腳本都執行失敗。這樣可以避免讓你創建一個部分有效的framework。
接著,用RW_MULTIPLATFORM_BUILD_IN_PROGRESS變量決定是否循環調用腳本,如果有該變量,則退出。
然後設定一些變量。該framework的名字與項目的名字一樣。也就是RWUIControls,另外,靜態lib的名字是libRWUIControls.a。
接下來,用腳本設置一些函數,這些函數一會項目就會用到,把下面的代碼加到腳本的底部。
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作為參數,例如iPhone7.0,然後創建靜態lib,大多數參數直接傳到當前的構建工作中來,不同的是設置ONLY_ACTIVE_ARCH來確保為當前SDK構建所有的結構。
make_fat_library使用lipo將兩個靜態庫合並為一個,其參數為兩個靜態庫和結果的輸出位置。從這裡了解更多關於lipo的知識。
為了使用這兩個方法,接下來腳本將定義更多你要用到的變量,你需要知道其他SDK是什麼,例如,iphoneos7.0應該對應iphonesimulator7.0,反過來也一樣。你也需要找到該SDK對應的編譯目錄。
把下面的代碼添加到腳本的底部。
# 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,這個正則表達式取出字符串開頭不包含數字的那些字符,因此,其結果是iphoneos 或 iphonesimulator。
這個正則表達式取出SDK_NAME中表示版本用的數字,7.0或6.1等。
這裡用簡單的字符串比較來將iphonesimulator 轉換為iphoneos,反過來也一樣。
從構建好的工程的目錄路徑的末尾找出平台名稱,將其替換為其他平台。這樣可以確保為其他平台構建的目錄可以被找到。這是將兩個靜態庫合並的關鍵部分。
現在你可以啟動腳本為其他平台編譯,然後得到合並兩靜態庫的結果。
在腳本最後添加下面的代碼:
# 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}"
首先,調用你之前定義好的函數為其他平台編譯
如果你現在正在為模擬器編譯,那麼Xcode會默認只在該系統對應的結構下編譯,例如i386 或 x86_64。為了在這兩個結構下都進行編譯,這裡調用了build_static_library,基於iphonesimulator SDK重新編譯,確保這兩個結構都進行了編譯。
最後調用make_fat_library將在當前編譯目錄下的靜態lib同在其他目錄下地lib合並,依次實現支持多結構的FAT靜態庫。這個被放到了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拷貝到用戶的桌面上,這一步是可選的,但我發現這樣做可以很方便的存取framework。
選擇Framework集合方案(aggregate scheme),按下cmd+B編譯該framework。
這一步將構建並在你的桌面上存放一個RWUIControls.framework。
為了檢查一下我們的多平台編譯真的成功了,啟動終端,導航到桌面上的framework,像下面一樣:
$ cd ~/Desktop/RWUIControls.framework $ RWUIControls.framework xcrun lipo -info RWUIControls
第一條指令導航到framework中,第二行使用lipo指令從RWUIControls靜態庫中得到需要的信息,這將列出存在於該庫中的所有片段。
這裡你可以看到,一共有五種片段:i386, x86_64, arm7, arm7s 和 arm64,正如你在編譯時設定的那樣。如果你之前使用lipo –info指令,你可以看到這些片段的一個分組。
OK,你已經有了framework,你也有了庫。它們可以提供一種優雅的方法來解決你迄今為止還沒有遇到過的問題,但是做這些的意義是什麼呢?
使用framework的其中一個主要的優點是簡化使用,現在你將創建一個簡單的iOS應用,並使用你剛剛創建好的RWUIControls.framework。
使用Xcode創建一個新工程,選擇File/New/Project,然後選擇iOS/Application/Single View Application,將新工程命名為ImageViewer,設置為僅僅用於iPhone,將其保存到與之前兩個工程同樣的目錄下。這個應用將展示一張圖片,允許用戶使用RWKnobControl旋轉圖片。
在你之前下載的壓縮文件中找到ImageViewer目錄,這裡面只有一個圖片文件,把這個圖片文件sampleImage.jpg從Finder中拖到Xcode的ImageViewer組中。
選中Copy items into destination group’s folder,點擊Finish完成導入操作。
導入一個framework的步驟幾乎相同,將RWUIControls.framework從桌面拖到Xcode中的Frameworks組下。同樣,確保選中了Copy items into destination group’s folder。
打開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
設置了一組私有屬性來持有UIImageView和RWKnobControl。
創建一個UIImageView,將其放到合適的位置。
為Knob control設置一些屬性,包括添加值改變的事件監聽器。相應方法為rotationAngleChanged:方法。
rotationAngleChanged:方法簡單更新了UIImageView的transform屬性,讓圖片隨著knob control的移動而旋轉。
具體怎麼樣使用RWKnobControl,可以看一下上一篇教程,那裡解釋了怎麼樣去創建它。
編譯並運行,你就能看到一款簡單的應用,當你改變knob control的值時圖片就會旋轉。
你有沒有注意到RWUIControls的framework只包含了代碼和頭文件,其他的文件卻沒有被包含。例如,你沒有使用其他任何資源,比如圖片。這是iOS的一個限制,framework只能包含頭文件和靜態庫。
現在准備好,這篇教程要開始進階了。這一部分你將學到怎麼樣通過使用bundle整合資源,讓其可以隨著framework一起發布,進而突破這一限制。
你將創建一個新的UI控件——絲帶控件,作為RWUIControls庫的一部分。這個控件將在一個UIView的右上方展示一個絲帶圖片。
資源都會被添加到bundle中,這將是RWUIControls工程上的另一個目標。
打開UIControlDevApp工程,選擇RWUIControls子工程,點擊Add Target按鈕,導航到OS X/Framework and Library/Bundle。將新的Bundle命名為RWUIControlsResources,然後從framework選擇框中選擇Core Foundation。
這裡需要配置幾個編譯設置,因為你正在創建一個在iOS上使用的bundle,這與默認的OS X不同。選擇RWUIControlsResources目標,然後點擊Build Settings欄,搜索base sdk,選擇Base SDK這一行,按下delete鍵,這一步將OS X切換為iOS。
同時你需要將工程名稱改為RWUIControls。搜索product name,雙擊進入編輯模式,將${TARGET_NAME}替換為RWUIControls。
默認情況下,有兩種resolutions的圖片可以產生一些有趣的現象。例如,當你導入一個retina @2x版本的圖片時,普通版的和Retina版的將會合並成一個多resolution的TIFF(標簽圖像文件格式,Tagged Image File Format)。這不是一件好事。搜索hidpi將COMBINE_HIDPI_IMAGES設置為NO。
現在,你將確保當你編譯framework時,bundle也能被編譯並將framework作為依賴添加到集體目標中。選中Framework目標,選擇Build Phases欄,展開Target Dependencies面板,點擊 + 按鈕,選擇RWUIControlsResources目標將其添加為依賴。
現在,在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"
這條指令將拷貝構建好的bundle到用戶的桌面上。現在,編譯framework scheme,你會發現bundle在桌面上出現。
為了用這個新的bundle開發,你需要在示例項目中使用它,這意味著你必須既把它作為依賴添加到工程中,同時作為一個對象拷貝到項目中。
在項目導航欄中,選擇UIControlDevApp工程,點擊UIControlDevApp目標,展開RWUIControls工程的Product組,把RWUIControls.bundle拖到Copy Bundle Resources面板中的 Build Phases欄。
在Target Dependencies面板中,點擊+按鈕,添加新的依賴,然後選擇RWUIControlsResources。
上面的就是所有必需的配置工作了,從你之前下載的壓縮文件中將RWRibbon文件夾拖入到RWUIControls工程下RWUIControls組中。
選中Copy the items into the destination group’s folder,在對應的選擇框中打勾,確保它被添加到RWUIControls靜態lib目標中。
代碼中一個很重要的部分是你怎樣引用一張圖片。如果你看一下RWRibbonView.m文件中的addRibbonView方法,你將會看到相關的這一行代碼:
UIImage *image = [UIImage imageNamed:@"RWUIControls.bundle/RWRibbon"];
Bundle就像一個文件目錄,所以引用bundle中的一張圖片是非常簡單的。
將圖片添加到bundle中,選擇這張圖片,在右邊的面板中,通過選擇來表示它應該屬於RWUIControlsResources目標。
還記得我們說過要確保framework可以被訪問嗎?現在,你需要導出頭文件RWRibbon.h,在Target Membership面板中選擇該文件,然後從彈出視圖中選擇Public。
最後,你需要將頭文件引用添加到framework的頭文件中。打開RWUIControls.h添加下面這兩行:
// RWRibbon #import < RWUIControls/RWRibbonView.h>
在UIControlDevApp項目中打開RWViewController.m文件,在@interface後的大括號中添加下面的實例變量聲明。
RWRibbonView *_ribbonView;
在viewDidLoad:的末尾添加下面的代碼來創建一個絲帶視圖:
// 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。你將看到新的絲帶控件出現在應用的下方。
我要向你分享的最後一件事是怎麼樣在其他應用中使用這個新的bundle,例如,你之前創建的ImageViewer應用。
開始之前,確保你的bundle和framework都是最新版本的,選擇Framework scheme然後按下cmd+B編譯。
打開ImageViewer工程,找到Frameworks組中的RWUIControls.framework項目,然後將其刪除,選擇Move to Trash。然後將RWUIControls.framework從你的桌面上拖到Frameworks組中。這是必須的,因為此時的framework已經與你第一次導入時的framework大不相同了。
Note:如果Xcode拒絕讓你添加framework,這可能是因為你並沒有真正將之前版本的framework刪除到廢紙簍。如果是因為這樣的話,從Finder中ImageViewer目錄下刪除framework然後重新嘗試。
導入bundle,簡單將其從桌面上拖到ImageViewer組中。選中Copy items into destination group’s folder,選中對應的選擇框,確保它被添加到ImageViewer目標中。
接下來你要將絲帶添加到可以旋轉的圖片上。因此,在RWViewController.m文件中代碼要有一些簡單的變動。
打開該文件,將屬性imageView的類型從UIImageView變為RWRibbonView:
@property (nonatomic, strong) RWRibbonView *imageView;
將viewDidLoad方法中第一部分,負責創建並配置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];
編譯並運行該項目,現在該項目中你同時使用了RWUIControls framework下的RWKnobControl和RWRibbonView。
在本篇教程中,你學到了關於創建一個framework並在你的iOS app中使用所需的一切知識,包括開發一個framework的最好的方式,以及怎麼樣使用bundle來共享資源。
有沒有一個你喜歡的功能在多個app中使用了呢?現在你所學到的概念可以幫你創建一個可復用的庫,使你的編碼更加簡單。Framework提供了一種優雅的方式來獲得庫中的代碼,讓你在寫一個炫酷的app的時候,可以靈活地獲取到你需要的一切。
完整工程的源碼被放到了Github上,每一步都有一個commit。或者你可以從這裡下載完整的壓縮文件。
(本文為CocoaChina組織翻譯,本譯文權利歸譯者所有,未經允許禁止轉載。)