云端 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 序列化测试需要通讯录权限吗?

不需要。示例只创建内存中的联系人并序列化数据,不查询或保存系统通讯录。访问真实通讯录是另一条需要权限的流程。

为什么不直接比较两次导出的 vCard 文件?

属性顺序、换行和折行方式可能变化。应先解析文件,再按字段契约比较姓名、号码和标签;只有明确规定字节格式时才增加字节级断言。

把下一次构建放到云端工位

整台 Mac mini 独享,非虚拟机。按任务选择租期,实际可用状态以控制台实时返回为准。

立即租用云端 Mac

先核对配置,再安排任务

查看 Desk M4 的计费周期与附加项,或按上手文档准备第一次连接。