Swift Digest
ST-0014 | Swift Evolution

Swift Testing での画像添付(Apple プラットフォーム)

Image attachments in Swift Testing (Apple platforms)

Proposal
ST-0014
Authors
Jonathan Grynspan
Review Manager
Maarten Engels
Status
Implemented (Swift 6.3)

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

01 何が問題だったのか

Swift Testing には Swift 6.2 で attachment の仕組み(ST-0009)が導入され、テスト中に生成した任意のデータをファイルとしてレポートに残せるようになりました。UI のレンダリング結果や CI 上での失敗時のスクリーンショットなど、画像を添付したい ケースは多くあります。

しかし ST-0009 の枠組みのままでは、画像を添付するためにテスト作者が自分で CGImageUIImage を JPEG / PNG などのバイト列にエンコードする必要があります。これは画像添付では非常に頻出する作業であり、各テスト作者がエンコード処理を毎回書くのは煩雑です。画像形式の指定や品質の調整についても、Attachable への適合の中で個別に組み立てる必要がありました。

画像の添付はテストでよく行われる操作なので、テスティングライブラリ側で CGImage / CIImage / NSImage / UIImage といったシステム標準の画像型を直接 Attachment.record(...) に渡せるようにし、画像形式の選択や品質指定もまとめて面倒を見る仕組みが求められていました。

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

Apple プラットフォーム向けに、画像を直接 attachment として扱う仕組みを導入します。具体的には次の 3 つです。

  • 画像型を Attachment に渡せるようにするためのプロトコル AttachableAsCGImage
  • 添付時に画像形式や品質を指定するための型 AttachableImageFormat
  • 上記を受け取る Attachment.init(...)Attachment.record(...) のオーバーロード

これらは macOS 11.0 / iOS 14.0 / watchOS 7.0 / tvOS 14.0 以降で利用できます。本 Proposal は Apple プラットフォームのみが対象で、Windows などへの対応は今後の見通しとして扱われています。

AttachableAsCGImage プロトコル

CGImage として取り出せる画像」を表すプロトコルとして AttachableAsCGImage が追加されます。Attachable を直接継承するのではなく、テスティングライブラリ側でこのプロトコルに適合した値を画像のバイト列に変換するためのフックです。

@available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *)
public protocol AttachableAsCGImage {
  /// An instance of `CGImage` representing this image.
  ///
  /// - Throws: Any error that prevents the creation of an image.
  var attachableCGImage: CGImage { get throws }
}

このプロトコルには、システム標準の次の画像型があらかじめ適合しています。

  • CGImage
  • CIImage
  • NSImage(macOS)
  • UIImage(iOS, watchOS, tvOS, visionOS, Mac Catalyst)

CGImage.attachableCGImageself をそのまま返し、それ以外の型は内部で保持している CGImage を取り出すか、必要に応じてその場でレンダリングします。テスト作者は通常、自分で AttachableAsCGImage への適合を書く必要はなく、別の表現の画像を扱いたい場合は事前に上記のいずれかの型に変換します。

なお、適合する型は将来追加される可能性があり、その判断は Testing Workgroup が行います。

画像を添付する

Attachment に、AttachableAsCGImage に適合する値を直接渡せる初期化子と record(...) のオーバーロードが追加されます。

@available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *)
extension Attachment {
  public init<T>(
    _ attachableValue: T,
    named preferredName: String? = nil,
    as imageFormat: AttachableImageFormat? = nil,
    sourceLocation: SourceLocation = #_sourceLocation
  ) where AttachableValue == _AttachableImageWrapper<T>

  public static func record<T>(
    _ image: T,
    named preferredName: String? = nil,
    as imageFormat: AttachableImageFormat? = nil,
    sourceLocation: SourceLocation = #_sourceLocation
  ) where AttachableValue == _AttachableImageWrapper<T>
}

_AttachableImageWrapper<Image> はジェネリクスの都合で API シグネチャに現れる内部のラッパー型で、Sendable かつ AttachableWrapper に適合しています。テスト作者がこの型を直接組み立てる必要はありません。

imageFormat は出力する画像の形式を指定するパラメータで、nil の場合はテスティングライブラリが選びます。nil を渡したときの選択は、preferredName の拡張子に応じた形式が利用可能ならそれを使い、利用可能な拡張子が指定されていなければライブラリが適切な形式を自動的に選ぶ、という挙動になります。

AttachableImageFormat で形式と品質を指定する

画像形式と、必要に応じてエンコード品質を抽象的に表す型として AttachableImageFormat が追加されます。PNG と JPEG は常に利用可能で、それ以外の形式はプラットフォームに依存します。Apple プラットフォームでは、Image I/O フレームワークの CGImageDestinationCopyTypeIdentifiers() が返す形式が利用できます。

@available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *)
public struct AttachableImageFormat: Sendable {
  /// The encoding quality to use for this image format.
  public var encodingQuality: Float { get }
}

encodingQuality0.0(最低品質)から 1.0(最高品質)までの値で、形式が可変品質エンコーディングをサポートしない場合は無視されます。

PNG と JPEG については、よく使われる形式として静的プロパティと関数が用意されています。

@available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *)
extension AttachableImageFormat {
  public static var png: Self { get }
  public static var jpeg: Self { get } // 最高品質の JPEG
  public static func jpeg(withEncodingQuality encodingQuality: Float) -> Self
}

たとえば 50% の品質で JPEG として保存したい場合は .jpeg(withEncodingQuality: 0.5) を指定します。

Apple プラットフォーム向けには UTType を受け取る初期化子も用意されており、Image I/O が対応する任意の形式を選べます。UTType.image に適合しない型を渡した場合の挙動は未定義です。

@available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *)
extension AttachableImageFormat {
  public var contentType: UTType { get }

  public init(_ contentType: UTType, encodingQuality: Float = 1.0)
}

利用例

たとえば SwiftUI のビューを画像としてレンダリングし、PNG として添付するには次のように書けます。

import Testing
import UIKit
import SwiftUI

@MainActor @Test func `attaching a SwiftUI view as an image`() throws {
  let myView: some View = ...
  let image = try #require(ImageRenderer(content: myView).uiImage)
  Attachment.record(image, named: "my view", as: .png)
  // OR: Attachment.record(image, named: "my view.png")
}

as: .png を明示する代わりに、preferredName の拡張子(my view.png)から PNG を選ばせることもできます。

03 今後の見通し

将来の発展として、以下のようなアイデアが挙げられています。

  • SwiftUI.ImageSwiftUI.GraphicsContext.ResolvedImage の添付対応。これらは CGImage を直接持たない型ですが、SwiftUI.ImageSwiftUI.View に適合していることを利用して、SwiftUI.ImageRenderer 経由で CGImage を得るアプローチが考えられています。SwiftUI.View 全般を扱うために、本 Proposal の _AttachableImageWrapper<Image> に相当する _AttachableViewWrapper<View> を導入する案も挙がっています。
  • Windows 向けの画像型対応。Windows には GDI / GDI+ / Windows Imaging Component (WIC) / Direct2D と複数世代の画像ライブラリがありますが、GDI 以外は C++ や COM ベースで、Swift から直接扱うのが難しいという課題があります。Apple の Core Graphics とソース互換ではないため、別のプロトコル設計が必要になります。実験的には GDI と GDI+ の一部に対応した HBITMAP / HICON の添付サポートが Swift Testing にあり、より新しいライブラリへの拡張は今後の検討対象です。
  • X11 系の画像型(Qt の QImage や GTK の GdkPixbuf など)の添付対応。GUI レイヤのライブラリは Linux に常に存在するわけではないため、Swift ツールチェイン本体で扱うのは難しく、将来 swift-x11 / swift-wayland / swift-qt / swift-gtk のような専用パッケージが現れた場合にそちらで担う可能性が示されています。
  • Android の android.graphics.Bitmap 対応。Android NDK の AndroidBitmap_compress() を使う方向ですが、適切なサポートには swift-java への依存が必要になる可能性があり、Android Workgroup と連携して進めることが想定されています。
  • 画像ではなく PDF として出力する経路の追加。Core Graphics の API で技術的には可能なものの、現時点では十分な需要が確認できていないとされています。

これらはいずれも将来の構想であり、実現を約束するものではありません。