在開發的時候,我們有時會需要在不同的 target 裡重複使用同一段程式碼。甚麼是 target 呢?在 Xcode 裡,target 包含了應用程式、擴充套件、測試套件、以及 framework 與 library 等幾種套件 (bundle)。一個 target 沒有辦法直接使用另一個 target 的程式碼,即使這兩個 target 都是屬於同一個應用程式。比如說,如果我現在有兩個 target,一個是叫做 MyNotes 的應用程式,另一個是 MyNotes 的分享擴充套件,叫做 Save to MyNotes,像這樣的話:
當我在 MyNotes 裡面新增了一個檔案,並在其中宣告一個叫做 Note
的 struct
:
我是沒有辦法在 Save to MyNotes 裡面使用這個 Note
的,會出現「找不到 identifier」的錯誤:
如果要使用 Note
的話,最快的方法就是把 Note.swift 這個檔案也加到 Save to MyNotes 裡面。也就是說,點選 Note.swift 之後,在右邊欄的 File Inspector 中的 Target Membership 列表中,勾選想要加入的 target:
錯誤就消失了:
怎麼會這樣呢?因為雖然 Note.swift 這個檔案現在還是放在 MyNotes 的資料夾裡面,但是 Xcode 不管在編譯 MyNotes 還是 Save to MyNotes 的時候,都會把它一起編譯進去。這種情況下,我們最好把它拿出來放到另一個資料夾裡,才能正確反映出它不只是屬於 MyNotes 的:
到這裡為止,大概就是一般在不同 target 間重用程式碼的做法了。然而,這個做法僅限於同一專案內的不同 target,而且很難整理。怎麼說呢?因為要把某個檔案加入到某個 target 裡面的時候,這個檔案所使用到的所有名稱 (identifier) 所在的檔案也都要一併加入到 target 裡面才行。比如說,如果我們今天要重用的是一個叫做 ParentViewController
的 view controller subclass,而它裡面包含了這段程式碼:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
super.prepare(for: segue, sender: sender)
if let childVC = segue.destination as? ChildViewController {
childVC.delegate = self
}
}
那麼,我們也必須把宣告 ChildViewController
的檔案也加入跟 ParentViewController
檔案一樣的 target 裡面,否則就會編譯錯誤:
看到這有如精神分裂的 compiler 錯誤訊息了嗎?一方面跟你說找不到 ChildViewController
這個東西,一方面又說 childVC
已經被定義但從來沒被用過。照理說,如果找不到型別的話,第二個錯誤訊息根本不會出現。這邊之所以有兩個衝突的錯誤訊息,是因為它們是針對不同 target 的錯誤訊息。
結果是,我們可能其實只想重用 ParentViewController
而已,卻必須連 ChildViewController
都一起打包進所有的 target。檔案一多,每個 target 就會一起都變肥大。而且每當我們動其中一個檔案的時候,所有有包含該檔案的 target 就必須要重新編譯,大幅增加編譯時間。有沒有更好的方法呢?
有的!這個方法叫做 framework。
Framework 的使用
初學者最常碰到的 framework,不外乎來自 Apple 官方 (Foundation、UIKit、CoreGraphics⋯⋯) 或者 CocoaPods (Alamofire、SwiftyJSON、Realm⋯⋯)。在 Swift 裡,我們透過 import
陳述來引用 framework:
import UIKit
以此來使用該 framework 中的程式碼。
其實,我們在這裡已經是在重用整個 UIKit
的程式碼了!然而,你會發現,我們並不需要在每個引用 UIKit
的 target 編譯時都同時去編譯 UIKit
,因為它已經編譯好了。同時,我們也完全不需要動到 Target Membership 列表。我們所要做的,就只是在每個檔案的開頭去 import
該檔案所需的 framework,以及針對非官方 framework 去內嵌 (embedding) 與連結(linking) 而已。內嵌與連結這兩個動作聽起來複雜,但其實只是拖拉檔案而已。
使用 framework 很簡單,但要自己做一個呢?你可能會感到驚訝,因為創造一個 framework 不只不困難,其實還蠻好玩的!
建立一個 Framework
現在,我們就來試著建立一個專案內 framework。如圖,在點選專案之後,打開 project and targets list,再點擊下方的加號按鈕。
此時會跳出一個範本選擇視窗,往下拉,在 Framework & Library 的區段裡選擇 Cocoa Touch Framework。旁邊有一個 Cocoa Touch Static Library 看起來跟它很像,但那是給 Objective-C 用的,而且不能包含 xib、圖片等資源。
點擊 “Next” 之後,會出現這個命名視窗。這裡我們要打包成 framework 的是 ParentViewController
,而為了辨識它是屬於 MyNotes 的 class,我們在它的名稱前面加上 “MN” 前綴,成為 “MNParentViewController”:
點擊 “Finish” 之後,會發現 target 列表裡面多了叫做 “MNParentViewController” 的 framework,而且 Project Navigator 裡也多出了同樣叫做 “MNParentViewController” 的資料夾。
接下來,點擊 MyNotes 的 target,並往下拉,找到 “Embedded Binaries” 與 “Linked Frameworks and Libraries” 兩項。非官方 Framework 必須要同時出現在這兩個列表裡面才能夠被使用,而因為剛剛新建 MNParentViewController 的時候已經選擇了把它內嵌到 MyNotes 裡面,它應該在兩個列表裡都已經出現。
接著,把要打包的檔案拉到屬於該 target 的資料夾底下。這裡是 MNParentViewController 資料夾:
拉過去之後,你會發現該檔案的 Target Membership 已經自動更新為只屬於該 framework 了:
我們在這裡不再需要去勾選其它要使用該 class
的 target,因為我們之後會透過 import
去使用它。然而,現在我們還沒準備好!首先,我們需要將要給別的 target 用的東西用 open
或 public
給標示起來;不然的話,在沒有加任何存取權關鍵字的情況下,宣告出來的東西就會預設是 internal
,亦即只有在同 target 內部才可以存取。而 open
跟 public
的差別是在於,open
只有 class
能用,指「開放給別的 target 去繼承該 class」的意思。所以你不想被繼承的 class
與所有其它東西就都得用 public
來標示:
public class ParentViewController: UIViewController {
// 有 override 的所有 class 成員都必須要標上跟 class 本身一樣的存取權關鍵字。
override public func viewDidLoad() {
super.viewDidLoad()
}
}
確認沒問題之後,就可以開始編譯了。首先選擇要編譯的 framework 的 scheme:
然後按 Command + B 開始編譯。編譯成功之後,就可以在同一個專案的其它 target 去使用該 framework 了!使用方法就跟使用所有其它的 framework 並無二致:
import UIKit
// 在此引用我們新建的 framework。
import MNParentViewController
class ViewController: UIViewController {
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
super.prepare(for: segue, sender: sender)
// 這裡就可以使用該 framework 裡宣告的東西了。
if let parentVC = segue.destination as? ParentViewController {
}
}
}
簡單嗎?
Framework 的好處
其實 framework 的好處絕不僅限於在不同 target 間重用程式碼很方便而已。Framework 的最大用處,其實是讓你的專案更模組化 (modular)。比如說,在同一個 target 內,有用的存取權關鍵字實際上只有 private
跟 fileprivate
而已。我們只能用 fileprivate
限制某個宣告於某個檔案裡,沒有辦法限制某個宣告於某些檔案裡,除非我們使用 framework 並且將該宣告標為 internal
。
再來,由於我們所建立的 framework 是獨立編譯的,記得我們沒有把裡面的檔案加入任何其它 target 嗎? 這樣一來,整個專案的編譯效率都會提升。比方說,如果我動了 ParentViewController
的宣告的程式碼,那麼它所屬的 target 就會被 compiler 標示為需要重新編譯。這時,如果 ParentViewController
是直接在主應用程式 target 裡的話,那麼整個應用程式都會需要被重新編譯。然而,如果我們已經用一個 framework 去把 ParentViewController
打包起來的話,那就只有這個 framework 需要被重新編譯。
同樣而言,所有的資源也是可以用 framework 來模組化的。一般最常用到的資源不外乎是 storyboard 跟圖片等等,而要怎麼把這些東西也放到模組裡呢?這裡假設我們在 Main.storyboard 裡有一個屬於 ParentViewController
的場景:
注意右邊 identity inspector 裡 Custom Class 的區段。如果 class 跟 storyboard 檔案不在同一個 target 裡的話,裡面的 Module 必須指向該 class 所在的 framework,否則在執行時會報錯。
確認有選取該場景後,執行選單列 Editor 選單裡的 Refactor to Storyboard…。
接著會跳出一個新增檔案的視窗,我們要在這裡把新切割出來的 storyboard 檔案加入到 MNParentViewController 這個 framework 裡。首先,把它命名為 “ParentViewController.storyboard”,然後在 Group 選單裡選擇要加入的 framework 所擁有的資料夾,上方的檔案路徑跟底下的 target 就會自動更新了。
按下 Save 之後,我們會發現新的 storyboard 檔案出現在 MNParentViewController 資料夾裡面,而且它的 target membership 列表裡只有 MNParentViewController 被勾選。
選取 ParentViewController
的場景,並開啟 identity inspector。此時已經可以勾選 “Inherit Module From Target” 了,因為此 storyboard 的所屬 target 跟 ParentViewController
是一樣的,都在 MNParentViewController 這個 framework 裡面:
如此,就完成了將 storyboard 切割並打包到 framework 裡的動作。
這樣做的好處,首先是讓主要的 storyboard 檔案不會對過多模組(主要是 framework)產生依賴,而讓複雜度上升。再來是切割了 storyboard 之後,在 Xcode 中載入 storyboard 的時間會減少。而因為將切割後的 storyboard 打包到個別的 framework 裡面,連編譯的效率也會跟著變好。至於 xcassets 檔案的打包方法呢,其實是跟 storyboard 蠻類似的,但這裡就先交給讀者自己去嘗試了。
結論
Xcode 的 framework 本身就是一種模組 (module)。平常我們寫程式時最基本的模組包括 func
、class
、struct
等的宣告,相對應的限制存取關鍵字是 private
。更進階的是個別的檔案,限制存取關鍵字是 fileprivate
。最高階的就是像 framework 的各種 target 了,限制存取關鍵字是 internal
。
使用 framework 來重構專案有許多好處:
- 更大的重用範圍:可以同時給不同的專案使用。
- 更彈性的存取控制:可以將多個檔案(包括程式碼與資源檔)一起打包起來,並用
internal
限制外部存取。 - 減少編譯時間:防止一個檔案的更動導致所有用到該檔案的 target 都要重新編譯。
- 減少記憶體用量:動態連結的 framework 如果路徑相同的話,在執行時不會被重複載入到記憶體。在 iOS 上,這主要是同一 app 底下的不同 target 會有的情況。
看到這裡,你會不會想試試看把自己專案裡的一些檔案用 framework 打包起來呢?