雲端 Mac 上的 iOS vCard 匯出驗收:多值欄位、跳脫與回讀

雲端 Mac 上的 iOS vCard 匯出驗收:多值欄位、跳脫與回讀

你將客戶聯絡人匯出成 .vcf,分享面板能正常開啟,匯入後卻只剩一組電話號碼,姓名中的分號也移了位置。這類問題不能只靠截圖驗收。先在雲端 Mac 上跑通「產生資料 → 重新解析 → 逐欄位比較」,再到 iOS 檢查分享與匯入流程,就能將格式錯誤與介面問題分開處理。

先定義需要保留的資料

本文驗證的是應用程式記憶體模型到 vCard 的轉換,不會讀取真實通訊錄。使用合成的聯絡人資料,涵蓋中文字元、重音字元、多組電話號碼與自訂標籤,避免個人資料進入建置日誌。

第一版契約可以很精簡:姓氏與名字必須原樣讀回;電話號碼保留輸入字串;自訂標籤與號碼的對應關係不能改變;空白欄位不能憑空套用業務預設值。號碼正規化應交由獨立邏輯處理,不要在匯出時順手刪除開頭的加號。

檔案能解析,只能證明語法可接受;回讀後的欄位一致,才能證明這條轉換路徑沒有遺失你在意的資料。

暫時不加入聯絡人照片、備註與群組。這些資料的容量、存取或相容性限制各不相同,應另外擴充測試樣本,不要讓一次失敗同時牽涉所有欄位。

使用系統序列化器建立基準

在已安裝 Xcode 命令列工具的雲端 Mac 上,將以下內容儲存為 vcard-check.swift。這段程式只使用 Foundation 與 Contacts,不會呼叫通訊錄查詢介面,也不需要申請通訊錄存取權限。

import Foundation
import Contacts

struct CheckFailure: Error {
    let field: String
}

func require(_ condition: Bool, _ field: String) throws {
    if !condition {
        throw CheckFailure(field: field)
    }
}

func phones(_ contact: CNContact) -> [String: String] {
    var result: [String: String] = [:]
    for item in contact.phoneNumbers {
        result[item.label ?? ""] = item.value.stringValue
    }
    return result
}

do {
    let source = CNMutableContact()
    source.familyName = "林;测试"
    source.givenName = "Zoë,研发"
    source.phoneNumbers = [
        CNLabeledValue(
            label: "build-primary",
            value: CNPhoneNumber(stringValue: "+12025550101")
        ),
        CNLabeledValue(
            label: "build-secondary",
            value: CNPhoneNumber(stringValue: "+12025550102")
        )
    ]

    let data = try CNContactVCardSerialization.data(with: [source])
    try data.write(to: URL(fileURLWithPath: "contact.vcf"),
                   options: .atomic)

    let contacts = try CNContactVCardSerialization.contacts(with: data)
    try require(contacts.count == 1, "contact-count")
    let restored = contacts[0]
    try require(restored.familyName == source.familyName, "family-name")
    try require(restored.givenName == source.givenName, "given-name")
    try require(restored.phoneNumbers.count == 2, "phone-count")
    try require(phones(restored) == phones(source), "phone-values")
} catch {
    FileHandle.standardError.write(Data("vCard check failed\n".utf8))
    exit(1)
}

執行後應檢查程序的結束碼,而不是搜尋「成功」字樣:

mkdir -p artifacts/vcard
cd artifacts/vcard
xcrun swift ../../vcard-check.swift

此範例假設指令碼位於儲存庫根目錄。只有命令回傳零才繼續執行流水線;若失敗,就停止後續交付。實際專案應將斷言放進測試目標,讓報告顯示具體失敗的欄位;不要直接印出聯絡人的原始內容。

將特殊字元拆成獨立測試樣本

上面的逗號與分號是刻意加入的。這些字元在 vCard 結構中具有特定意義,直接拼接文字容易將一個姓名拆成多個組成部分。應使用系統序列化器產生基準,不要先手寫替換規則,再用測試證明自己的規則正確。

接著增加三組輸入:含有反斜線的姓名、含有換行的公司名稱,以及足夠長的多位元組字串。每組只改變一個面向,回讀對應屬性,並檢查聯絡人總數仍為一,避免換行被誤判成額外屬性或紀錄。

長行不等於字元數多

vCard 的實體行可能會摺行,中文字元所占的位元組長度也不同。不要以「每隔固定數量的 Swift 字元插入換行」來實作摺行,更不要使用逐行 split 自製解析器。若你維護獨立編碼器,應依其宣告支援的 vCard 版本檢查位元組層級的規則,並將輸出交給 Contacts 回讀。

範例依標籤建立字典,只適用於標籤不重複的測試資料。正式環境的聯絡人可能有重複標籤;擴充測試案例時,應改為比較「標籤、號碼」二元組的多重集合,同時保留重複項目的數量。

區分語意一致與檔案一致

同一位聯絡人重新序列化後,屬性順序與摺行方式不一定適合直接拿來做檔案快照比對。應優先比較解析後的欄位,將檔案雜湊留給傳輸完整性檢查,而不是用來判斷聯絡人資料的語意是否一致。

批次匯出時,加入空陣列、單一聯絡人與多位聯絡人的樣本。使用測試專用的唯一姓名識別每筆紀錄,不要依賴解析結果的陣列順序。分別驗證紀錄數量、欄位值與多值項目數量,避免字典覆寫掩蓋資料遺失。

系統框架回讀自己產生的資料,也不能完整證明跨應用程式的互通性。若交付對象使用其他匯入工具,應保留同一批合成檔案,逐一驗收;不支援的標籤如何轉換,必須訂出明確契約,不能默默忽略。

接入流水線並補齊 iOS 驗收

在分享功能相關變更後執行快速格式檢查,並記錄系統與工具鏈版本。失敗時只封存合成的 .vcf 與欄位錯誤識別碼;成功後依流水線的保留政策清理目錄,不要將真實通訊錄當成測試資料。

iOS 端還要檢查副檔名、分享項目提供的資料類型,以及目標應用程式實際讀取到的檔案內容。匯入專用測試環境後,核對兩組電話號碼、自訂標籤與特殊字元,再清除測試紀錄。模擬器的結果不能取代目標裝置與目標匯入應用程式上的確認。

最後保留三項獨立結論:產生的資料能解析、核心欄位能回讀,以及分享後的檔案能由目標應用程式正確匯入。哪一項失敗,就回到對應層級排查,不要藉由刪除特殊字元或只保留第一組號碼,讓測試表面上通過。

常見問題

執行 vCard 序列化測試需要通訊錄權限嗎?

不需要。範例只建立記憶體內的聯絡人並轉換資料,不查詢或儲存系統通訊錄。存取真實通訊錄則需要另外處理授權。

為什麼不直接比對兩次匯出的檔案?

屬性順序、換行及折行方式可能改變。應先解析檔案,再依欄位契約比較姓名、電話與標籤;只有明確要求位元組格式時才加入對應斷言。

把下一次建置工作放到雲端工作站

整台 Mac mini 專屬使用,非虛擬機。依工作需求選擇租用期間,實際可用狀態以控制台即時回報為準。

立即租用雲端 Mac

先確認規格,再安排工作

查看 Desk M4 的計費週期與加購項目,或參考入門指南,準備首次連線。