Swift Digest
ST-0023 | Swift Evolution

Transferable な添付物

Transferable Attachments

Proposal
ST-0023
Authors
Julia Vashchenko, Jonathan Grynspan
Review Manager
Stuart Montgomery
Status
Implemented (Swift 6.4)

このダイジェストはClaude Opus 4.7 / 4.8によって生成されたものです(License)。原文はこちら

01 何が問題だったのか

ST-0009 で導入された Attachment 型は、テストに任意のデータを添付するための仕組みです。Attachment には、添付対象が Attachable かつ Encodable、または Attachable かつ NSSecureCoding に適合する場合のために、バイト列への変換を肩代わりするデフォルト実装が用意されていました。

一方、Apple プラットフォームには CoreTransferable フレームワークがあり、その中の Transferable プロトコルは値とバイナリデータの相互変換を行うための標準的な仕組みとして広く使われています。SwiftUI や AppIntents をはじめ、多くの公開 API がこのプロトコルを前提に組み立てられており、すでに Transferable に適合した型を持っているケースは少なくありません。

しかし、これまでは Transferable に適合しているだけではテストに添付することができず、テスト作者は別途 Attachable 用のラッパーを書くなど、本来のテストロジックとは関係のない準備コードを毎回書く必要がありました。EncodableNSSecureCoding と同じように、Transferable についてもデフォルトの添付経路を用意しておくのが自然です。

02 どのように解決されるのか

Transferable に適合した値をそのまま Attachment に渡せるようにするための新しいイニシャライザが追加されます。内部的には、Transferable 値をラップする AttachableWrapper の実装(_AttachableTransferableWrapper)が用意され、添付時に Transferableexported(as:) を呼んでバイト列に変換します。

使い方

Attachment(exporting:as:named:sourceLocation:)Transferable 値と添付したいコンテントタイプを渡すだけで、テストに添付できます。exported(as:)async throws なので、このイニシャライザも async throws です。

import Testing
import CoreTransferable

@Test func menuNotEmpty() throws {
    let menu = FoodTruck.menu
    if menu.isEmpty {
        let attachment = try await Attachment(exporting: menu, as: .pdf)
        Attachment.record(attachment)
        Issue.record("The food truck's menu was empty")
    }
}

struct Menu: Transferable {
    static var transferRepresentation: some TransferRepresentation {
        DataRepresentation(exportedContentType: .pdf) { menu in try await menu.pdfData() }
    }
}

ここでは MenuTransferable に適合しており、PDF として書き出すための DataRepresentation を持っています。テスト側ではラッパー型を別途用意する必要はなく、exporting: 経由でそのまま添付できます。

新しいイニシャライザ

Attachment に追加されるイニシャライザは次のシグネチャです。

@available(macOS 15.2, iOS 18.2, tvOS 18.2, visionOS 2.2, watchOS 11.2, *)
extension Attachment {
  public init<T>(
    exporting transferableValue: T,
    as contentType: UTType? = nil,
    named preferredName: String? = nil,
    sourceLocation: SourceLocation = #_sourceLocation
  ) async throws where T: Transferable, AttachableValue == _AttachableTransferableWrapper<T>
}

各引数の役割は次のとおりです。

  • transferableValue: 添付したい Transferable 値。
  • contentType: 書き出しに使う UTTypenil の場合、テスティングライブラリは値の exportedContentTypes(_:) を呼び、そこから返ってきた中で UTType.data に適合する最初のものを選びます。
  • preferredName: 添付ファイルの希望ファイル名。nil の場合は値から妥当な名前が生成されます。
  • sourceLocation: 添付に関する issue を記録する際に使われる位置情報。

戻り値の型は Attachment<_AttachableTransferableWrapper<T>> になります。_AttachableTransferableWrapperAttachableWrapper に適合する内部型で、Transferable 値の保持と書き出しを引き受けます。

非同期・throws である理由

Transferableexported(as:) は時間のかかる処理になりうるため async、また失敗しうるため throws です。たとえば指定された UTType に対応する表現がない場合や、ディスク上のファイルに裏付けられた値を読み出す途中で I/O エラーが発生した場合などに失敗します。これらの失敗は、添付の組み立て時点でローカルに扱える形で throw され、Attachment.record(_:sourceLocation:) 自体は従来どおり同期で throws を伴わないシンプルな API のままに保たれます。