You export customer contacts as a .vcf file. The share sheet opens, but after import only one phone number remains, and the semicolon in a name has moved. Screenshots cannot validate this kind of problem. First run a “generate → reparse → compare each field” check on a cloud Mac, then test sharing and importing on iOS. This separates format errors from interface issues.
Define what must survive the export
This article tests the conversion from an app’s in-memory model to vCard. It does not read real contacts. Use synthetic contacts covering Chinese text, accented characters, multiple phone numbers, and custom labels to keep personal data out of build logs.
The initial contract can be small: family and given names must round-trip unchanged; phone numbers must retain their input strings; custom labels must stay associated with the correct numbers; and empty fields must not acquire application defaults. Handle number normalization in separate logic rather than casually stripping the leading plus sign during export.
A file that parses has only passed a syntax check. Matching fields after reparsing is what shows that this conversion path preserved the data you care about.
Leave photos, notes, and groups out for now. They have different limits around size, access, or compatibility. Extend the samples separately instead of making a single failure implicate every field.
Establish a baseline with the system serializer
On a cloud Mac with the Xcode command-line tools installed, save the following as vcard-check.swift. It uses only Foundation and Contacts, does not call contact-query APIs, and does not require permission to access 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)
}
After running it, check the process exit code rather than searching for a success message:
mkdir -p artifacts/vcard
cd artifacts/vcard
xcrun swift ../../vcard-check.swift
This example assumes the script is in the repository root. Continue the pipeline only if the command returns zero; stop subsequent delivery steps on failure. In a real project, put the assertions in a test target so the report identifies the field that failed. Do not print the raw contact data.
Give special characters their own test cases
The commas and semicolons above are deliberate. They have structural meaning in vCard, so assembling the text by direct concatenation can split a single name into multiple components. Use the system serializer to establish a baseline rather than writing replacement rules first and then proving your own rules correct.
Add three more sets of inputs: a name containing a backslash, a company name containing a newline, and a sufficiently long multibyte string. Change only one dimension in each set, read back the corresponding property, and check that the total contact count is still one. This catches newlines incorrectly treated as additional properties or records.
Line length is not character count
Physical lines in vCard can be folded, and Chinese characters do not all have the same byte length. Do not implement folding by inserting a newline after a fixed number of Swift characters, and do not build a parser by using split line by line. If you maintain a separate encoder, check the byte-level rules for the vCard version it claims to support, then feed its output back into Contacts.
The example builds a dictionary keyed by label, which works only for test data with unique labels. Production contacts can have duplicate labels. When expanding the cases, compare a multiset of “label, number” pairs instead, preserving the number of duplicate entries.
Separate semantic equality from file equality
Reserializing the same contact may change property order or line folding, making direct file snapshots unsuitable. Compare parsed fields first. Reserve file hashes for checking transfer integrity, not for deciding whether contact data is semantically equivalent.
For batch exports, add cases for an empty array, a single contact, and multiple contacts. Identify each record by a unique test-only name rather than relying on the order of the parsed array. Verify record counts, field values, and the number of entries in multivalued fields separately so dictionary overwrites cannot hide lost data.
A round trip through the same system framework is not complete proof of cross-application interoperability. If the recipients include other importers, save the same set of synthetic files and validate each importer separately. Define an explicit contract for how unsupported labels are converted; do not silently ignore them.
Add the check to CI and complete iOS validation
Run the quick format check after changes related to sharing, and record the OS and toolchain versions. On failure, archive only the synthetic .vcf files and field-error identifiers. After success, clean up the directory according to the pipeline’s retention policy. Do not use real contacts as test fixtures.
On iOS, also check the file extension, the data type provided by the share item, and the file contents the receiving app actually reads. Import into a dedicated test environment, verify both phone numbers, custom labels, and special characters, then remove the test records. Simulator results do not replace confirmation on the target device with the target importing app.
Keep three separate conclusions: the generated data parses, the core fields survive a round trip, and the shared file imports correctly into the target app. Investigate each failure at its own layer. Do not make the tests appear to pass by deleting special characters or keeping only the first phone number.
Frequently asked questions
Does the serialization test need access to contacts?
No. It creates contacts in memory and serializes data without querying or saving the system address book. Real address-book access requires a separate permission flow.
Why not compare exported files byte for byte?
Property order and line folding can change without changing the contact. Parse the file and compare field semantics first; add byte checks only for an explicit format contract.
Move your next build to a cloud workstation
An entire Mac mini, exclusively yours—not a virtual machine. Choose a rental term that fits your project; availability is confirmed in real time by the console.
Rent a Cloud Mac