在本次的教學中,我將展示如何利用 Xcode Target 控制建構 iOS App(以及 macOS、watchOS 和 tvOS)過程中的複雜性。如果開發者知道並非所有事情都要靠程式碼(如 Swift)來完成的話,就可以節省大量時間。像 Xcode 這樣的整合式開發環境 (IDEs) 提供了許多強大的工具,其中一個就是 Target,它可以讓開發者將過去在程式碼中(或是手動)完成的工作細節,分離到專案的配置設定中。我發現因為專案設定的數量眾多,許多開發者經常看著 Xcode 中長長的 Build Settings 列表,心裡難免會萌生想死的念頭。看完本篇教學後,你將能夠把程式碼整齊地組織到一個專案之中,並且能夠產生 iOS、macOS、watchOS 和 tvOS 所用的二進位檔。
如果開發者可以利用 Xcode 的功能,就有更多時間來做該做的事,例如使用架構設計模式 (MVVM)、戰術設計模式(工廠方法模式、外觀模式與轉接器模式)、物件導向技巧、協定導向技巧、泛型、委派等所有良好且紮實的開發觀念,來設計、撰寫及組織自己的程式碼。
開發者應該持續關注任何邏輯上組織相關程式碼的機會;相反,也將邏輯上不相關的程式碼分離。Xcode Target 可以讓你將相關的程式碼放到同一個專案中,同時將程式碼定位在 Apple 的不同平台:iOS、macOS、watchOS 和 tvOS。
在一般的任務上,例如變更 App 的名稱、版本編號、幫助資訊、App 圖示、或是版權說明等,開發者都應該要能夠使用 Xcode 的工具。開發者不應該使用駭客技巧、金字塔厄運 (pyramid-of-doom) 形式的 if
語句、或是將數值或邏輯寫死 (hardcoding) 等方式來解決這些任務。讓我們將這些無趣卻必要的步驟從程式碼分離出來,這時候 Target 就能派上用場。
我在這裡討論的協定是使用 Xcode “target” 與 “scheme” 的混用,我知道不同的人都有不同的做事方式,但我發現這是對我最有效的方式,但這不是唯一的方法。
我知道我(可能)可以使用 Xcode 的 Workspace 讓方法更加通用,但這是一篇教學;你需要先掌握 Target 及 Scheme 的概念,才可以建構更好、更新、更大的方案。而且,太通用的東西有時未必很有用。
簡介
在 Xcode 創建一個新的專案時,會同時創建一個 target。我創建了一個 iOS Single View App 模版的 Xcode 專案,並且命名為 “Xcode Targets”,你可以從 GitHub 下載它。讓我們來看看專案配置設定/選項的儲存方式。
根據 Xcode 文件的說明:
在專案導覽器中選取專案名稱,你就可以開啟專案編輯器。
我們照著這個描述操作,就會出現以下這個畫面:
在我們正式開始學習本次教學之前,請將 Xcode 文件中的兩個概念銘記在心。第一,考慮一下:
在 Target 層級定義的建置設定會覆寫任何在專案層級建置設定的值,因此,Target 層級配置的優先權比專案層級配置高。
第二,請注意:
一個 Target 包含了用來建構 App 的指令(以建構設置和建構階段的形式),它也繼承了專案的建構設置。雖然大多數開發者都不太需要更改這些設定,但你仍然可以透過在 Target 層級指定不同的設置,來覆寫任何專案的建構設置。
我並不同意「大多數開發者都不太需要更改這些設定」的說法。我們將會改變設定,這會是本次教學中一個主要的議題。
關於「配置」的說明
Apple 列出了這樣的說明:
當你創建專案時,Xcode 會提供兩個標準的專案層級建置設定:除錯 (debug) 及發佈 (release) ⋯⋯ 這兩個建置設定應該足夠滿足你開發 App 的需要,大多數的開發者永遠不需要變更大部份建置設定的數值。
我想補充一點,除了在專案層級外,你還可以在每個 target 中變更除錯與發佈設定中的數值。或許是因為我開發 Apple App 的資歷較久,而且我做的大多數 App 都比較複雜,我發現自己和同事都經常需要改變建置設定之中的數值。我也發現在專案和 target 層級的每個建置設定中,都有一個 Debug
和 Release
選項,這使我們非常混亂而難以管理。看看下面這張圖,而這些只是部分可用的選項:
所以當面對眾多的「除錯/發佈」選項時,開發者有可能在無意且輕易地將除錯及發佈選項搞混,或是一不小心就設定到衝突的選項。我們馬上就會看到,我寧願將所有的發佈選項放到同一個 target 之中,而將所有的除錯選項放到另一個 target。
將除錯設定與發佈設定分開
創建了如上面所述的專案後,我所做的第一件事就是將預設的 target 重新命名,如下圖所示:
讓我簡單的說明一下 schemes。要組織專案設定有許多種方法,而我想要保持本次教學文章清晰明瞭,讓我再次的引用 Xcode 文件。
當你打開現有的專案(或是創建一個新專案),Xcode 會自動為每個 target 創造一個 scheme,預設的 scheme 會用專案名稱來命名。
你可以透過我上面的連結來瞭解 scheme。因為預設的 scheme 在創建專案的時候就產生了,所以我需要重新將它命名為與預設 target 同樣的名字。
請注意,我將 Autocreate schemes 的選項保持打勾狀態。
現在,讓我們將 “Xcode Targets – Release” scheme 中的 Build Configuration 選項設定為 Release
,以切合其名稱。到 Set the active scheme 控制選項,也就是上一個影片中開始的地方,選取 Edit Scheme…,並將 Debug
改成 Release
:
然後,我們也要為除錯創建一個新的 target(及 scheme)。請跟隨以下影片看看我們如何創建並命名一個新的 target:
剛剛勾選 Autocreate schemes 的選項現在就發揮了效果,創建了一個新的 scheme;然而,它沒有幫我將 target 的重新命名為 “Xcode Targets – Debug”。請確認你亦有將自動創建出來的 scheme 名字從 “Xcode Targets – Release copy” 改為 “Xcode Targets – Debug”,如上面的 gif 檔所展示那樣。然後,將 “Xcode Targets – Debug” scheme 的 Build Configuration 改為 Debug
;它有可能已經被設定為 Debug
,但還是確認一下比較安全。
為了在 debug 和 release 兩者設定之中做切換,你所需要做的就是切換/設定活動 scheme。非常重要的一點,請確認你有將所有 Build Settings 中 Release
的設定限制在 “Xcode Targets – Release” target 之中;同樣地也將所有 Build Settings 中 Debug
的設定限制在 “Xcode Targets – Debug” target 之中,我們將會在下面看到如何實作這件事。
關於 Info.plist 的說明
當你複製(創造)了一個新的 target,Xcode 會創造一個 Info.plist
檔,檔案會為 target 取個名字,放在某個位置,但可能不是你想要的名字和位置:
我喜歡在 Xcode 專案中使用 $(SRCROOT)
巨集來標準化及組織我的檔案位置。為了保持使用 “Xcode”,我在 “Xcode Targets – Release” target 的 Info.plist
檔如此設定了 Build Settings :
而 “Xcode Targets – Debug” target,我就把其 Info.plist
放了在這裡:
$(SRCROOT)/Xcode Targets/Debug/Info-Debug.plist
我強烈建議你做完變更後清理 Project Navigator 設定。(按住 control 並點擊 Project Navigator 中的 “Xcode Targets” 資料夾,並使用選單中的 Add Files to “Xcode Targets”… 的指令即可。)看一下範例專案你就會明白了。
發佈與除錯的依賴關係
我曾經開發過許多使用 C++ 函式庫的 iOS 與 macOS Xcode App,為了提供優化產品程式碼,我只在公開發佈 App 時,才會引入函式庫的發佈版本。在開發及除錯階段,我想函式庫的除錯版本將所有符號連接到 App 中,讓我可以設置中斷點 (breakpoint),並逐步執行程式碼。
為了產出,我切換到 “Xcode Targets – Release” target,並將發佈版本的函式庫拖拉到 Build Phases -> Link Binary With Libraries,像是這樣:
在除錯及開發的時候:
請記住,如果我從連結的函式庫中呼叫程式碼,我需要標頭檔 (header file) (.h, .hpp) 的路徑。我通常在 Build Settings -> Search Paths -> User Header Search Paths 中指定它們:
如果我負責在 Xcode 管理函式庫的程式碼,我可以在專案中使用與本教學相同的分離關注結構。你將會在下文看到範例。
假設我有 iOS 框架的發佈和除錯版本。對於發佈版本,我只要點選我的 “Xcode Targets – Release” target,並將發佈框架版本拖放到 General -> Embedded Binaries 即可。而對除錯版本,我只需要點選我的 “Xcode Targets – Debug” target,並同樣將除錯框架版本拖到我的 target 中即可。
如此一來,發佈及除錯的依賴關係都被我的 target 協定分開得一乾二淨。我們待會將再深入討論依賴關係。
條件編譯
有使用過或是還在使用 Objective-C 的人,都習慣在 Build Settings -> Apple LLVM 9.0 – Preprocessing -> Preprocessor Macros 下,利用 Xcode 中定義像 DEBUG
這種相關符號。如果你想透過 Swift 達到一樣的行為,你就需要使用 Apple 所說的條件編譯:
條件編譯的區塊讓程式碼能夠根據一個或多個編譯條件被編譯。
你可以使用這個連結中列出的任何符號,但我將會討論客製化符號的使用。Swift 依然尊重 DEBUG
,但是如果你想要使用它、或其他語言內建的符號、又或是客製化的符號,你就需要現在到 Build Settings -> Swift Compiler – Custom Flags -> Active Compilation Conditions 定義它們。
假設我登入 (LOG
) 了發佈版本,並創建了一個審計追蹤 (audit trail)(也就是說我在追蹤登入紀錄)。請確認一下在我的 “Xcode Targets – Release” target 之中有甚麼東西:
假設我在開發過程中並不擔心審計,檢查一下我的 “Xcode Targets – Debug” target 中的內容:
我開發過不少 iOS 和 macOS 的 Xcode 專案,它們使用的 C++ 程式碼可在多個平台上執行,包括 Unix/Linux 版本、Microsoft Windows 和 macOS。有些時候事情會變得棘手,舉例來說,我遇到只能在 Windows 上編譯與執行的程式碼,而無法在 Mac 編譯與執行,反之亦然。我盡量減少這種情況發生,但還是會以完成事情為優先目標。必要的時候,我會使用處理器巨集、或是現在 Apple 在 Swift 中所說的條件編譯,像是 WIN
、MAC
、LOG
以及 DEBUG
。
當我呼叫可移植 C++ 程式碼時,一些像是 MAC
的內容會同時在發佈及除錯版本中被定義,如此一來,我就能在 macOS(或 Windows)選擇編譯或不編譯、只能建構及執行或建構及執行、可移植或不可移植的核心程式碼。請記住,我可以在條件編譯中使用邏輯運算子(像是 !
)。
我通常會使用 DEBUG
進行報告,並根據錯誤的情況採取行動。當我的程式碼需要審計追蹤(像是處理敏感資料)時,我也經常使用 LOG
。
例如,如果我需要處理 Mac 中特定的核心程式碼,以及除錯和日誌記錄,我就會使用以下通用而可執行的 ViewController.swift
文件:
class ViewController: UIViewController { var loginAttempts: Int = 0 @IBAction func loginButtonPressed(_ sender: Any) { #if LOG print("Login button pressed") loginAttempts += 1 if loginAttempts > 3 { print("HACK ATTEMPTED?") // CALL 911! } #endif } override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view, typically from a nib. #if WIN print("Use Windows code...") #elseif MAC print("Use Mac code...") #endif } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() // Dispose of any resources that can be recreated. #if DEBUG print("ViewController::didReceiveMemoryWarning") #endif } } // end class ViewController
假設我使用 “Xcode Targets – Release” target 來建構與發佈我的 App,以下就是上文程式碼區塊在終端機的輸出:
Use Mac code... Login button pressed Login button pressed Login button pressed Login button pressed HACK ATTEMPTED?
這邊主要想傳達的內容是,你可以依照自己的需求來設定多或少的編譯條件,並且可以在不同 target 之中設定這些條件的不同組合。雖然我看不慣這些條件編譯的程式碼,但是在現實世界工作中,遇到複雜的情況時它們就是你的好夥伴。我更喜歡在打開日誌(即 print
語句)等情況下使用條件編譯。
不同 App 的版本/商標
如果你已經把程式碼許可給想在 App 內展示自己商標 (branding)(像是客製商標/圖標、及特定的產品名稱)的合作伙伴/經銷商,那麼該怎麼辦呢?在 iOS App Store 之中,Apple 曾經說過他們不太喜歡圖標看起來不同、但內容其實一樣的 App。不過你確實可以將同一個 App 發佈免費、中級和高級功能等不同版本,以提高產品價值來提升銷售。而在 Apple Developer Enterprise Program 的環境中,對於 iOS App 的商標就更加彈性。如果是 macOS 的開發,Apple 允許你不透過 Mac App Store 來建構、商標及發佈,所以你有更大的彈性來完成 App。
不同的圖標、圖像、文字及分層功能是否需要不同的 Xcode 專案?一般而言是不需要的,只要使用 target 就可以了。我已經能夠支援多達 10 個不同版本。
為了展示如何利用 target 來處理商標,我創建了一個基於 Cocoa App 模板的 macOS Xcode 專案,命名為 “Xcode Manage Config”,你可以在 GitHub 上下載。
我的專案展示了如何在 App 中抽象化並客製下列項目:
- 幫助資訊 (Help bundle)
- 名稱 (Name)
- 版本編號 (Version number)
- 圖標 (Icon)
- 版權說明 (Copyright text)
上述所有都不須駭客操作就可完成(也就是說不須使用 if 語句及寫死的方式)。看看範例專案裡 ViewController.swift
檔案中的程式碼及註解,應該就能瞭解我是如何從程式碼抽象化上述 5 項資料到 target:
import Cocoa let reverseDomain = "us.microit." class ViewController: NSViewController { @IBOutlet weak var logoImage: NSImageView! @IBOutlet weak var productName: NSTextField! @IBOutlet weak var copyright: NSTextField! @IBOutlet weak var version: NSTextField! var helpPath: String = "" override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. // Get the product name by stripping out the // reverse domain name. let bundleIdentifier = Bundle.main.bundleIdentifier?.replacingOccurrences(of: reverseDomain, with: "") // Display the product name. productName.stringValue = bundleIdentifier! // Build app icon set name using concatenation. let appIconSetName:String = bundleIdentifier! + "AppIcon" // Get and display the product logo. logoImage.image = NSImage(named: NSImage.Name(rawValue: appIconSetName)) // Get the copyright statement. let copyrightString = Bundle.main.object(forInfoDictionaryKey: "NSHumanReadableCopyright") // Display the product copyright. copyright.stringValue = copyrightString! as! String // Get the product version. let versionString = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") // Display the version. version.stringValue = "Version: " + (versionString! as! String) // Build and save the path to help files. helpPath = Bundle.main.path(forResource: "index", ofType: "html", inDirectory: bundleIdentifier!)! } // end func viewDidLoad() @IBAction func invokeHelp(_ sender: Any) { // Open help in default browser. NSWorkspace.shared.openFile(helpPath) } override var representedObject: Any? { didSet { // Update the view, if already loaded. } } } // end class ViewController
讓我們建構及執行程式碼,來看看 target 如何能夠從同一個軟體的兩個不同商標,來區分兩個不同的許可方。讓我們建構及執行 “Acme – Release” target:
然後,建構並執行 “Emca – Release” target:
你可以看到沒有任何東西是寫死的,除了我的反向域名稱之外(這自 1999 年以來都沒有改動過)。它可以輕易地被抽象化到一個 plist
中,請原諒我這小小的違法行為。請注意, macOS 辨認到每個 target 的官方 App 圖標:
What if we click on the “Help” button in either target’s app? This is help customized to the “Acme – Release” target, running in Safari:
假如我們點擊任兩個 target 中 App 的 “Help” 按鈕會發生甚麼事?以下是 “Acme – Release” target 中客製化幫助資訊在 Safari 執行的樣子:
而 “Emca – Release” target 中的幫助資訊是不一樣的:
幫助資訊 (The help bundle)
幫助資訊通常只是一個包含 HTML
檔案/資源的路徑,我將名為 “Emca” 與 “Acme” 的幫助資料夾拖放到 Project Navigator 之中。拖放兩個資料夾時,Xcode 都有彈出提醒,我只會展示拖放 “Acme” 資料夾時的提示。請注意,我只有將 “Acme” 資料夾設定為 “Acme – Release” target 的成員:
如果你點擊 Xcode Project Navigator 中的「Emca」資料夾,你將會看到其 Target Membership 只有勾選 “Emca – Release” target:
我看過有些幫助資訊大至幾 MB,所以如果你堅持在專案中加入實際的幫助資源,請確保你不會將不必要的資訊包含在內,像是不要將 Acme 的幫助資訊包含在 Emca 之內,反之亦然。順帶一提,不管你相信與否,我聽過許多客戶堅持需要從 App 中獲得實質的幫助。
如果你計劃為 “Xcode Manage Config” 專案中的一個或多個 target 建構、簽署及創造簽署安裝程序,你需要執行幾個額外的步驟。首先,記得到 Build Phases -> Copy Bundle Resources,並在 “Acme – Release” target 中添加 “Acme” 資料夾以作複製,接著以同樣的方式在 “Emca – Release” target 中添加 “Emca” 資料夾以作複製。再次提醒,兩個產品,兩個不同的 target。
App 名稱與版本編號
App 的名稱及版本編號在每個 target 中都不同,要設定名稱及版本編號,你可以選擇 target,並到 General -> Identity 來設定。我這邊只向你展示如何設定 “Emca – Release” target 的數值:
App 圖標
在這裡,你可以看到我已經為 “Acme – Release” 設定了 App 的圖標,並且準備為 “Emca – Release” 建立新的 App 圖標集,這是在所有 target 共享的資產目錄 (Asset Catalog) 中:
看看上面的程式碼,我獲取了與程式碼無關的 Bundle Identifier,並且附加了 “AppIcon” 來獲得適合當前目標的 App 圖標集。
版權說明
就如我在第一個範例專案所說,我為這個專案的兩個 target 安排了 Info.plist
檔案。如你所預期,我們有一個 Info-Acme.plist
對應 “Acme – Release” target,及一個 Info-Emca.plist
對應 “Emca – Release” target。變更版權說明與程式碼無關。要突顯每個 target 中的 plist
檔案,你只需要點選 Copyright (human-readable)
字串數值,並且如此編輯:
剛剛一系列的範例說明了如何利用 target 讓 App 的程式碼和資源保持集中,並且讓你有彈性地進行改動,例如為同一個 App 建立多種版本,而無需使用寫死、編寫腳本或寫下金字塔厄運式語句等方式。
不同 App 平台的不同依賴關係
多年來,我已經設計、編碼、使用並累積了許多優良且紮實的 C++ 程式碼,這些程式碼可以用於解決各種問題、或是模擬大部分的日常情境,例如是消息傳遞、統計測試、數據操作等。多年來,我學會了在浪費數百小時重覆又重覆地編寫程式碼前,先找尋可以重用的現有程式碼。
這和 target 有什麼關係?我將會在下文說明,如何於 macOS 和 iOS 平台,在有 target 的 Xcode 專案中重複使用現有程式碼(如果你動一下腦筋,也可以包含 tvOS 及 watchOS 平台)。如此一來,同ㄧ個專案可以允許你建構二進位檔,並且在四個不同的 Apple App 中重複使用它們。
我可以向你展示如何透過框架來完成這件事,這雖然符合你所需,不過為了保持本篇教學簡單、目標清晰,我只會建構函式庫。如果你想要ㄧ起操作,你可以從 GitHub 下載基於函式庫的 “Xcode Cocoa Library” 專案。
依照這些設定來創造ㄧ個新的 Xcode 專案:
點選 Next 並按照下列配置:
點選 Next 並儲存在你喜歡的位置,在 Project Navigator 之中,找到 Xcode_Cocoa_Library.m
檔案,並將它重新命名為 Xcode_Cocoa_Library.mm
,以確保 C++ 和 Objective-C++ 可以支援它。
如同我們先前所做的,複製 “Xcode Cocoa Library” target,並將它與相關的 scheme 重新命名為 “Xcode iOS Library”。在這個時候,你的專案看起來應該像這樣:
點擊 “Xcode Cocoa Library” target,到 Build Settings 看一下 Architectures 的部分,你會看到函式庫已經配置在以 Intel 為基礎的電腦的 macOS 中執行:
你會看到已經成功建置 target 了。
點擊 “Xcode iOS Library” target,到 Build Settings 看一下 Architectures 的部分,改變 Base SDK 為最新的 iOS 版本,就像是這樣:
你應該注意到函式庫已經配置為在基於 ARM 架構電腦的 iOS 中執行。建構 “Xcode iOS Library” target,可以看到 target 成功地被建置。
讓我們在專案中加入一些程式碼,以確保我們可以編寫 C++ 和 Objective-C++ 程式碼(請注意,很多時候我都會添加紮實的現有 C++ .h 和 .cpp 檔案,然後混合一些 Objective-C 和 Objective-C++ 程式碼,並且能夠在我的 macOS、watchOS 和 iOS App 中重覆使用大量程式碼。)。
以下是我們的 Xcode_Cocoa_Library.h
檔案:
#import <Foundation/Foundation.h> #include <iostream> @interface Xcode_Cocoa_Library : NSObject - (void)helloWorld; @end class Parent { public: virtual void soundOff() { std::cout << "This is the parent class" << std::endl; } }; class Child: public Parent { public: virtual void soundOff() { std::cout << "This is the child class" << std::endl; } };
而這是 Xcode_Cocoa_Library.mm
檔案:
#import "Xcode_Cocoa_Library.h" @implementation Xcode_Cocoa_Library - (void)helloWorld { Child child; child.soundOff(); NSLog(@"Hello, world!"); } @end
加入程式碼後,建置兩個 target,成功的話結果會是這樣:
如果建置失敗的話,這些函式庫的名字會變成紅色。
我不會在這邊展示如何在 Swift 專案中使用這些函式庫,這已經超出了本次教學的範疇。但你不用擔心,我已經多次將函式庫(以及靜態函式庫和框架)在 Swift 專案中合併使用(一般來說會使用到包裝器、橋接標頭檔、和函式庫的標頭檔)。
我想指出的是,只要利用 target,你不需要任何駭客技巧,就可以重複使用並組織程式碼,並能夠在多個平台上建置,同時還擁有良好的維護性及延伸性。
真正通用的 App 程式碼
看看上一節討論 “Xcode Cocoa Library” 專案中的這張圖:
我可以創建兩個新的 target,一個用於 tvOS,另一個用於 watchOS。如果我適當地組織程式碼,隨時準備抽象化程式碼,我就可以在一個專案中建構、維護和延伸功能到 macOS、iOS、tvOS 和 watchOS 平台了!
結論
實際上,我目前擁有的函式庫能夠讓我在 macOS、iOS、tvOS 和 watchOS App 中,重用我多年累積的 C、C++、Objective-C、Objective-C++ 和 Swift 程式碼。我提及所有這些語言的原因,是因為 Xcode 的 target 和 scheme 讓我能夠重用很多以前編寫的程式碼;而且我已經能夠在多個 Apple 平台(iOS、macOS、watchOS 和 tvOS)上重用我的遺留下來的程式碼。
要做到這樣,只能規劃、進行需求分析、在編程前先設計、專注於抽象化程式碼、並在跨出第一步前仔細觀察。這樣就能夠透過實作類別的最佳練習等概念和技術、使用 MVVM 等架構設計模式、使用戰術設計模式(第一部、第二部和第三部)、以及使用物件導向和協議導向程式設計方法,來抽象化出這麼多的程式碼。
我正在使理論變得實用。你有很棒的工具,例如 Xcode 及它的 target 和 scheme。 不要成為一個「只要完成任務就好」的開發人員,花點時間閱讀 Xcode 提供的說明文件,看一些第三方書籍和影片,或是加入特殊興趣小組和社交媒體小組。從你的學習中收集及整理資訊,並且在日常工作中付諸實踐。不要停滯不前,要持續嘗試新事物並不斷突破極限。
如果你肯花些時間,你會發現像 Xcode 這樣的工具提供超出想像的功能。