設計稿中的標題使用了自訂字型,本機除錯時也完全正常,但自動建置產生的 App 安裝到測試裝置後,卻悄悄改用了系統字型。這類問題通常不會造成編譯失敗:字型可能未加入目標、Info.plist 中的相對路徑有誤,或程式碼使用了檔名,而不是字型內部的 PostScript 名稱。最可靠的做法不是繼續檢查專案目錄,而是在雲端 Mac 完成建置後,直接稽核最終的 App 套件。
先定義字型門禁要檢查什麼
字型門禁至少要回答四個問題:宣告的檔案是否存在、檔案能否由 Core Text 解析、內部名稱是否符合程式碼約定,以及多個檔案是否產生重複名稱。檢查對象應是 .app 目錄,而不是原始碼中的 Resources 資料夾,因為目標成員資格與 Copy Bundle Resources 階段都可能改變最終結果。
| 檢查項目 | 失敗代表的意義 | 建議處理方式 |
|---|---|---|
UIAppFonts 項目 |
建置設定未宣告字型 | 修正目標的 Info 設定 |
| App 套件內檔案 | 字型未複製到建置產物 | 檢查目標成員資格與資源階段 |
| PostScript 名稱 | 程式碼使用的名稱可能有誤 | 從字型描述元讀取真實名稱 |
| 名稱唯一性 | 字型家族或版本發生衝突 | 刪除重複檔案或明確定義取代關係 |
字型在介面上「看起來差不多」不能作為驗收依據。發生回退時,較短的英文文字可能不易察覺,但字寬變化會影響換行、按鈕尺寸與螢幕截圖比對。
建議在儲存庫中維護一份 ci/expected-fonts.txt,每行記錄一個允許使用的 PostScript 名稱。它是程式碼呼叫與資源檔案之間的穩定契約,不應記錄開發者電腦上安裝的所有字型。
從最終 App 套件擷取實際資訊
先透過現有建置流程產生 App 套件,再將路徑傳給檢查指令碼。Debug 與 Release 可能使用不同的資源設定,因此發布流程必須檢查實際準備交付的設定。指令碼不能先向系統註冊字型再進行驗證,否則機器上既有的同名字型可能掩蓋 App 套件中的缺漏。
以下 Swift 指令碼會讀取 Info.plist 中的 UIAppFonts,逐項確認檔案存在,並透過 Core Text 擷取內部名稱。將它儲存為 ci/check_fonts.swift:
import Foundation
import CoreText
let arguments = CommandLine.arguments
guard arguments.count == 2 else {
FileHandle.standardError.write(Data("usage: check_fonts.swift /path/App.app
".utf8))
exit(64)
}
let appURL = URL(fileURLWithPath: arguments[1], isDirectory: true)
let plistURL = appURL.appendingPathComponent("Info.plist")
guard
let data = try? Data(contentsOf: plistURL),
let plist = try? PropertyListSerialization.propertyList(from: data) as? [String: Any],
let declared = plist["UIAppFonts"] as? [String],
!declared.isEmpty
else {
FileHandle.standardError.write(Data("UIAppFonts is missing or empty
".utf8))
exit(1)
}
var failed = false
var seen = Set<String>()
for relativePath in declared {
let url = appURL.appendingPathComponent(relativePath)
guard FileManager.default.fileExists(atPath: url.path) else {
FileHandle.standardError.write(Data("missing: \(relativePath)
".utf8))
failed = true
continue
}
let descriptors = CTFontManagerCreateFontDescriptorsFromURL(url as CFURL) as? [CTFontDescriptor] ?? []
if descriptors.isEmpty {
FileHandle.standardError.write(Data("unreadable: \(relativePath)
".utf8))
failed = true
}
for descriptor in descriptors {
let value = CTFontDescriptorCopyAttribute(descriptor, kCTFontNameAttribute)
guard let name = value as? String, !name.isEmpty else {
FileHandle.standardError.write(Data("unnamed: \(relativePath)
".utf8))
failed = true
continue
}
if !seen.insert(name).inserted {
FileHandle.standardError.write(Data("duplicate: \(name)
".utf8))
failed = true
}
print("\(name) \(relativePath)")
}
}
exit(failed ? 1 : 0)
執行時請使用建置產物的實際路徑:
xcrun swift ci/check_fonts.swift "$APP_PATH" | sort > build/actual-fonts.txt
diff -u ci/expected-fonts.txt build/actual-fonts.txt
如果清單只儲存名稱,可以在輸出後加上 cut -f1。關鍵是讓差異回傳非零結束碼,使流程立即停止。
固定 PostScript 名稱,而不是檔名
Headline-Bold.otf 只是磁碟上的檔名,字型內部名稱可能是 HeadlinePro-Bold。SwiftUI 的 Font.custom、UIKit 的字型初始化以及測試程式碼,都應使用內部名稱。重新命名字型檔案不會自動改變這個名稱,但供應商更新字型版本時,內部名稱卻可能發生變化。
為名稱變更設定明確的審查流程
當 actual-fonts.txt 與允許清單不一致時,不要直接在流程中自動接受新值。應先確認這項變更是否來自計畫內的升級,再搜尋專案中的字型呼叫。可以將字型名稱集中放在一個原始碼常數檔案中,避免散落於檢視程式碼與測試夾具。
同一個字型檔案也可能包含多個描述元。指令碼應保留所有名稱,而不是只取第一個。如果團隊確實需要可變字型,還應針對常用字重補上一組介面層級的冒煙測試,確認軸設定與設計基準一致。
補上執行階段與介面驗收
靜態門禁可以證明資源結構正確,但無法證明每個頁面都使用了正確的字重。建議新增一個僅供測試目標使用的字型清單頁面,涵蓋標題、內文、數字、中文標點與較長的英文單字,並在固定尺寸的模擬器上執行螢幕截圖驗收。
執行階段斷言應檢查字型實例的 fontName,不要只比較 point size。對於動態字型,測試還應至少涵蓋兩個內容尺寸類別,觀察放大後是否發生截斷。如果介面允許在字型缺失時回退,回退邏輯必須明確記錄在程式碼中;核心品牌字型則應在測試環境中直接觸發失敗。
進行平行測試時,每個工作都應使用獨立的建置目錄與結果目錄,避免某個分支產生的允許清單覆蓋另一個分支。字型檔案屬於輸入資源,不應由建置指令碼直接在原位置修改。
將檢查整合至可重現的流程
完整順序應為:清理隔離的建置目錄、執行目標設定的建置、定位唯一的 App 套件、執行字型結構檢查、比對允許清單,最後再執行介面測試。不要使用模糊的 find | head -1 選擇產物;同一目錄中存在多個 App 時,它可能會檢查到測試宿主或舊建置。
門禁失敗後,應保留 actual-fonts.txt、App 套件內的字型檔案清單與建置記錄片段,但不要上傳無關的憑證。排查時先查看缺失檔案,再檢查 UIAppFonts,最後核對內部名稱;依照這個順序,可以快速區分「未打包」與「呼叫名稱錯誤」。
字型升級獲得核准後,應在同一次變更中提交字型檔案、允許清單、集中管理的名稱常數與螢幕截圖基準。這樣回復任一提交時,都能還原一套完整狀態,也不會讓雲端 Mac 的建置結果依賴某台開發機預先安裝的字型。
常見問題
為什麼不能只確認專案目錄裡有字型檔?
專案檔案不代表已進入最終產物。應檢查建置或封存後的 App 套件,確認檔案已複製,而且 UIAppFonts 的每個項目都能解析。
SwiftUI 的 custom 字型名稱可以直接使用檔名嗎?
不應直接假設檔名可用。custom 通常需要字型內部的 PostScript 名稱,應從描述符讀取並與專案允許清單比對。
字型檢查應放在流水線哪個階段?
放在可安裝 App 套件生成之後、上傳或分發之前,確保檢查的是最終資源複製結果。
在雲端持續執行你的 Mac mini 工作流程
選擇 RunAMac M4、租用週期與部署節點,將開發、建置或實驗工作放到一台獨享實體主機上。