クラウドMacで検証するiOSのvCard書き出し:複数値と再読み込み

クラウドMacで検証するiOSのvCard書き出し:複数値と再読み込み

顧客の連絡先を .vcf に書き出すと、共有シートは開くのに、読み込み後は電話番号が1件しか残らず、氏名に含まれるセミコロンの位置まで変わっている。こうした問題は、スクリーンショットでは検証できません。まずクラウドMac上で「データ生成→再解析→フィールド単位の比較」を通し、その後にiOSで共有と読み込みを確認すれば、形式上の不備とUIの問題を切り分けられます。

保持すべきデータを先に定義する

この記事で検証するのは、アプリのメモリ上のモデルからvCardへの変換です。実際の連絡先は読み取りません。中国語、アクセント付き文字、複数の電話番号、カスタムラベルを含む合成データを使い、個人情報がビルドログに入り込むのを防ぎます。

最初の検証条件は、最小限で構いません。姓と名は元のまま戻ること、電話番号は入力文字列が保持されること、カスタムラベルと番号の対応が変わらないこと、空のフィールドに業務ロジック上のデフォルト値が勝手に入らないことを確認します。電話番号の正規化は別の処理に任せ、書き出しのついでに先頭のプラス記号を削除しないでください。

ファイルを解析できるだけでは、構文が受け入れられたことしか分かりません。再読み込みしたフィールドが一致して初めて、この変換経路で必要なデータが失われていないと確認できます。

顔写真、メモ、グループは、いったん対象外にします。それぞれ容量、アクセス、互換性の制約が異なるため、個別にサンプルを追加して検証すべきです。1件の失敗で、すべてのフィールドを調べることにならないようにします。

システムのシリアライザーで基準を作る

XcodeコマンドラインツールをインストールしたクラウドMacで、次の内容を 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の構造上の意味を持つため、文字列を単純に連結すると、1つの氏名が複数の要素に分割されることがあります。まずシステムのシリアライザーで基準を作りましょう。独自の置換ルールを先に書いて、そのルールの正しさを自分で追認する形にはしないでください。

さらに、バックスラッシュを含む氏名、改行を含む会社名、十分に長いマルチバイト文字列の3組を追加します。各ケースでは変更する要素を1つに絞り、対応するプロパティを再読み込みして確認します。連絡先の総数も1件のままであることを確認し、改行が誤って別のプロパティやレコードとして扱われていないかを検出します。

長い行は文字数だけでは判断できない

vCardの物理行には折り返しが入ることがあり、中国語の文字もバイト長が一様ではありません。「Swiftの文字を一定数数えるごとに改行する」という方法で折り返しを実装しないでください。行単位の split で独自パーサーを作るのも避けます。独自のエンコーダーを保守している場合は、対応を明示しているvCardバージョンのバイト単位の規則を確認し、その出力をContactsで再読み込みしてください。

この例ではラベルをキーにして辞書を作っていますが、この方法が使えるのはラベルが重複しないテストデータだけです。実際の連絡先には同じラベルが複数存在する場合があります。ケースを拡張するときは、「ラベル、電話番号」のペアを多重集合として比較し、重複する項目の個数も保持してください。

意味の一致とファイルの一致を区別する

同じ連絡先を再シリアライズしても、プロパティの順序や行の折り返し方が変わることがあるため、ファイルをそのままスナップショット比較する方法は適していない場合があります。まず解析後のフィールドを比較してください。ファイルハッシュは転送時の完全性確認に使い、連絡先の意味的な一致を判定するためには使いません。

一括書き出しでは、空の配列、連絡先1件、複数の連絡先のケースを追加します。各レコードはテスト専用の一意な氏名で識別し、解析結果の配列順序には依存しないようにします。レコード数、フィールド値、複数値フィールドの項目数をそれぞれ検証し、辞書の上書きによって欠落が見えなくなるのを防ぎます。

システムフレームワークで書き出して同じフレームワークで再読み込みできても、アプリ間の相互運用性を完全に証明したことにはなりません。引き渡し先に別のインポーターが含まれる場合は、同じ合成ファイル一式を保存し、個別に検証してください。未対応ラベルをどう変換するかは明確な仕様にし、黙って無視しないことが重要です。

パイプラインに組み込み、iOSでの検証を補う

共有機能に関係する変更の後に、高速な形式チェックを実行するようにし、OSとツールチェーンのバージョンを記録します。失敗時にアーカイブするのは、合成データの .vcf とフィールドエラーの識別子だけにします。成功後はパイプラインの保持ポリシーに従ってディレクトリを削除し、実際の連絡先をテストフィクスチャとして使わないでください。

iOS側では、ファイル拡張子、共有アイテムが提供するデータ型、共有先アプリが実際に読み取ったファイルの内容も確認します。専用のテスト環境に取り込んだ後、2つの電話番号、カスタムラベル、特殊文字を照合し、テスト用レコードを削除します。シミュレーターでの結果だけでは、対象デバイスと対象の読み込み先アプリでの確認を代替できません。

最後に、結果は3つに分けて管理します。生成データを解析できること、主要フィールドを再読み込みできること、共有後のファイルを対象アプリが正しく取り込めることです。失敗した項目に対応する層に戻って原因を調べてください。特殊文字を削除したり、最初の電話番号だけを残したりして、見かけ上テストを通すべきではありません。

よくある質問

このテストに連絡先へのアクセス許可は必要ですか?

不要です。メモリ上で連絡先を作成してデータを変換するだけで、システムの連絡先を検索・保存しません。

書き出したファイルをそのまま比較しないのはなぜですか?

属性の順序や改行、折り返しが変わる場合があるためです。まず解析し、氏名、番号、ラベルを項目ごとの契約で比較します。

次のビルドはクラウドのワークステーションで

Mac miniを1台まるごと専有できます。仮想マシンではありません。タスクに合わせて利用期間を選べます。実際の稼働状況はコントロールパネルでご確認ください。

クラウドMacをレンタル

構成を確認して、タスクの準備を

Desk M4の利用期間とオプションを確認するか、初回接続の手順をご覧ください。