Вы экспортируете контакты клиентов в .vcf: меню «Поделиться» открывается, но после импорта остаётся только один номер, а точка с запятой в имени оказывается не на своём месте. Скриншотами такие проблемы не проверить. Сначала выполните на облачном Mac цикл «генерация данных → повторный разбор → сравнение каждого поля», а затем проверьте передачу файла и импорт в iOS. Это позволит отделить ошибки формата от проблем интерфейса.
Сначала определите, какие данные нужно сохранить
В этой статье проверяется преобразование внутренней модели данных приложения в vCard без чтения реальной адресной книги. Используйте синтетические контакты с китайскими символами, буквами с диакритикой, несколькими номерами и пользовательскими метками, чтобы персональные данные не попадали в журналы сборки.
Для начала достаточно небольшого набора требований: фамилия и имя должны возвращаться без изменений; номера телефонов должны сохранять исходные строки; связь пользовательских меток с номерами не должна меняться; пустые поля не должны самопроизвольно заполняться значениями по умолчанию, принятыми в приложении. Нормализацию номеров следует вынести в отдельную логику, а не попутно удалять начальный плюс при экспорте.
Если файл разбирается, это подтверждает лишь допустимость синтаксиса. Только совпадение полей после обратного чтения доказывает, что на этом пути преобразования не потерялись важные для вас данные.
Пока не включайте фотографии, заметки и группы. У них свои ограничения по объёму, доступу и совместимости. Для них стоит отдельно расширить набор примеров, чтобы один сбой не затрагивал сразу все поля.
Создайте эталон с помощью системного сериализатора
На облачном Mac с установленными инструментами командной строки Xcode сохраните следующий код в файл vcard-check.swift. Он использует только Foundation и Contacts, не вызывает API для запросов к адресной книге и не требует разрешения на доступ к контактам.
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 также проверьте расширение файла, тип данных, предоставляемый элементом для отправки, и содержимое файла, которое фактически получает целевое приложение. После импорта в отдельную тестовую среду проверьте оба номера, пользовательские метки и специальные символы, затем удалите тестовые записи. Результаты в симуляторе не заменяют проверку на целевом устройстве и в целевом приложении для импорта.
В итоге зафиксируйте три независимых результата: сгенерированные данные разбираются, основные поля корректно считываются обратно, а переданный файл правильно импортируется целевым приложением. Если один из этапов не проходит, ищите причину на соответствующем уровне. Не удаляйте специальные символы и не оставляйте только первый номер ради формально успешного теста.
Часто задаваемые вопросы
Нужно ли разрешение на доступ к контактам?
Нет. Скрипт создаёт контакты в памяти и преобразует данные, не читая и не изменяя системную адресную книгу.
Почему не сравнивать файлы побайтово?
Порядок свойств и переносы строк могут меняться без потери данных. Сначала разберите файл и сравните значения полей по заданному контракту.
Перенесите следующую сборку на облачный Mac
Вам выделяется целый Mac mini — не виртуальная машина. Выберите срок аренды под задачу; актуальный статус доступности смотрите в консоли.
Арендовать облачный Mac