Transferable入門
Transferableは、共有やデータ転送のためのプロトコルです。これを用いてモデルをシリアライズおよびデシリアライズする方法を宣言的に記述することができます。モデルを拡張して文字列や画像として他のアプリと共有したり、カスタム宣言したデータタイプを作成し共有することもできます。
WWDC22(iOS 16, etc.)での初登場時はドラッグ&ドロップやコピー&ペーストなどをサポートする文脈で紹介されましたが、WWDC24ではApp Intents/Apple Intelligenceの文脈でもこのTransferableが登場しました [1]。
本記事ではこのTransferableの入門として、WWDC22の"Meet Transferable"というセッションの内容をまとめます。同セッションではTransferableについて以下のような内容が解説されています。
- 一般的なユースケースにおけるAPIの使用方法
- 高度な機能を活用した動作のカスタマイズ方法
- 大量のデータを処理する場合に、メモリ効率を最適化する方法
以下、文章、画像はWWDCの基本的に同セッションからの引用です。適宜要約したり、順序を並び替えたり、見出しを追加したりしています。
作るアプリの説明
ドラッグ&ドロップ、コピー&ペースト、その他のアプリの機能をサポートする宣言的方法について説明します。
SwiftUIやMacのアプリ開発とは別にコンピュータサイエンスにおける女性技術者に興味がありました。ヒーローを知ることは重要ですよね。女性技術者を表示・追加・編集できるカタログアプリを作りました。
エンジニアや科学者のプロフィールです。
このアプリはアプリ間でシームレスに
科学者のプロフィールや知識の移動やドラッグ&ドロップをサポートすることを目指します。
また、初めてアプリでwatchOSとの共有をサポートします。
ウォッチからパーソナリティ・プロフィールを共有したいとの要望がありました。
SwiftUIによってiOSとMacでも共有できます。
今年ShareSheetのデザインも一新されました。
すべてを可能にするにはアプリ内や他のアプリケーションのレシーバーへの送信をサポートするモデルが必要です。
- プロフィールのstructには一人の人格に関するすべての情報が含まれています。
- すべてのプロフィールをアーカイブして友達と共有できます。
- 技術者の情報を文字で保存し、
- ビデオも添付できます。
これらのモデル・タイプをすべて共有する新しい容易な方法があります。Transferableを紹介します!
これは共有やデータ転送のためにモデルをシリアライズおよびデシリアライズする方法を記述するための、Swift初の宣言的な方法です。
アジェンダ
- Anatomy of Transferable
- Transferableとは何か、それを使うときに裏で何が起こっているのか
- Conforming custom types
- カスタムタイプへの適合方法
- Advanced tips and tricks
- Transferableをカスタマイズして必要なことを行うのに役立つ高度なヒントやトリック
Anatomy of Transferable
送信するバイナリデータが何なのかを判断する
2つのアプリが起動しており、ユーザーがコピー&ペースト・ShareSheet・ドラッグまたは他のアプリの機能を使って、あるアプリから別のアプリに情報を渡したい場合、2つの異なるアプリ間で何かを送信するとき、このすべてのバイナリデータが行き交います。
このデータを送信する際に重要なのは、そのデータが何に対応しているかを判断することです。
テキストやビデオや、ある女性技術者のプロフィール、あるいは全体のアーカイブかもしれません。
Uniform type identifiers
データが何なのか説明するUTType
もあります。
アプリのバイナリデータの生成を見てみましょう。
他のアプリあるいは同じアプリ内で共有される情報をバイナリデータに、あるいはその逆に相互変換する方法と
バイナリデータの構造を表すコンテントタイプが必要です。
コンテントのタイプ–uniform type identifiersとは、異なるバイナリーの構造と抽象概念の識別子について記述するアップルの独自技術です。識別子はツリーで
カスタム識別子も定義できます。
例えば、プロフィールのバイナリデータ構造のカスタム識別子の宣言に使います。
宣言をInfo.plistファイルに加え、ファイル拡張子も加えます。
データがディスク上にある場合、システムはこれをあなたのアプリが開ける事をこの情報から知ります。
次にコードで宣言します。
import UniformTypeIdentifiers
// also declare the content type in the Info.plist
extension UTType {
static var profile: UTType = UTType(exportedAs: "com.example.profile")
}
コンテンツのタイプの学習には、このビデオをすすめます。
Uniform Type Identifiersとは何で、どう使うのか明快に説明しています。
Standard Transferable types
良いニュースは多くの標準タイプが
すでにTransferableに準拠しています。例えば、文字列・データ・URL、書式付き文字列・画像などです。
PasteButton
新しいSwiftUIのペーストボタンを使う数行のコードで、
プロフィールにペーストしたり、
コード全体
import SwiftUI
struct Profile {
private var funFacts: [String] = []
mutating func addFunFacts(_ newFunFacts: [String]) {
funFacts.append(newFunFacts)
}
}
struct PasteButtonView: View {
@State var profile = Profile()
var body: some View {
PasteButton(payloadType: String.self) { funFacts in
profile.addFunFacts(funFacts)
}
}
}
ビューから画像をドラッグしたり、
Finderや他のアプリからの画像のドロップをサポートします。
コード全体
import SwiftUI
struct PortraitView: View {
@State var portrait: Image
var body: some View {
portrait
.cornerRadius(8)
.draggable(portrait)
.dropDestination(payloadType: Image.self) { (images: [Image], _) in
if let image = images.first {
portrait = image
return true
}
return false
}
}
}
ShareLink
新しいShareLink
で、
ウォッチから共有を実装できます。
コード全体
import SwiftUI
struct Profile {
var name: String
}
struct ProfileView: View {
@State private var portrait: Image
var model: Profile
var body: some View {
VStack {
portrait
Text(model.name)
}
.toolbar {
ShareLink(item: portrait, preview: SharePreview(model.name))
}
}
}
Conforming custom types
Transferableとは何かとその使い方の基本を説明しました。
アプリのモデルにTransferable準拠を追加する方法を見てみましょう。
言及したように今回のアプリでは、
4つのモデルタイプを共有します。
文字列は既に対応しています。何も必要ありません。
1つのプロフィール、プロファイルのアーカイブ、およびビデオの共有ではどうでしょう。
Transferable
に準拠させるために実装するのはただ一つ。
TransferRepresentation
だけです。
extension Profile: Transferable {
static var transferRepresentation: some TransferRepresentation {
}
}
Transfer representations
モデルがどのように転送されるか記述します。
3つ重要なRepresentationがあります。
CodableRepresentation
DataRepresentation
FileRepresentation
それぞれ説明します。
最初は中心となるモデルのProfile
の構造です。
id・名前・略歴・趣味・ポートレイト・ビデオです。
import Foundation
struct Profile: Codable {
var id: UUID
var name: String
var bio: String
var funFacts: [String]
var video: URL?
var portrait: URL?
}
CodableRepresentation
既にCodable
に準拠しますので、Transferable
にはCodableRepresentation
を含めることができます。
Codable
はエンコーダでプロフィールをバイナリデータへ変換し、デコーダが解読します。デフォルトでJSONを使用しますが、自分のエンコーダとデコーダも使用できます。
Codable
プロトコルの詳細と、エンコーダとデコーダ仕組みは、
このプロトコルが最初に登場したWWDC18の「データを信頼できるものに」をご覧ください。
プロフィールに戻ると、Codableで必要な情報は希望のコンテンツのタイプのみです。
extension Profile: Codable, Transferable {
static var transferRepresentation: some TransferRepresentation {
CodableRepresentation(contentType: )
}
}
カスタム・フォーマットになるので、カスタムと宣言されたuniform type identifierを使います。
プロフィールとコンテンツタイプを加え完了です。
extension Profile: Codable, Transferable {
static var transferRepresentation: some TransferRepresentation {
CodableRepresentation(contentType: .profile)
}
}
// also declare the content type in the Info.plist
extension UTType {
static var profile: UTType = UTType(exportedAs: "com.example.profile")
}
プロフィールはTransferable
に準拠しています!
コード全体
import CoreTransferable
import UniformTypeIdentifiers
struct Profile: Codable {
var id: UUID
var name: String
var bio: String
var funFacts: [String]
var video: URL?
var portrait: URL?
}
extension Profile: Codable, Transferable {
static var transferRepresentation: some TransferRepresentation {
CodableRepresentation(contentType: .profile)
}
}
// also declare the content type in the Info.plist
extension UTType {
static var profile: UTType = UTType(exportedAs: "com.example.profile")
}
DataRepresentation
別のケースのProfilesArchiveです。
既にCSVデータ変換がサポートされていますので、リストをCSVファイルでエクスポートし、友達とのシェアしたり、別のマシンにインポートできます。
これ(convertToCSV()
)でArchiveはData
に相互変換できますので、DataRepresentation
に準拠したことになります。
中を覗いてみると、DataRepresentation
は変換関数を使って直接バイナリ表現を作り、
レシーバーのために値を再構築しています。
extension ProfilesArchive: Transferable {
static var transferRepresentation: some TransferRepresentation {
DataRepresentation(contentType: .commaSeparatedText) { archive in
try archive.convertToCSV()
} importing: { data in
try ProfilesArchive(csvData: data)
}
}
}
DataRepresentation
の使用でTransferable対応は容易です。2つの既にある関数initializer、またCSVコンバータを呼び出し利用するだけです。
コード全体
import CoreTransferable
import UniformTypeIdentifiers
struct ProfilesArchive {
init(csvData: Data) throws { }
func convertToCSV() throws -> Data { Data() }
}
extension ProfilesArchive: Transferable {
static var transferRepresentation: some TransferRepresentation {
DataRepresentation(contentType: .commaSeparatedText) { archive in
try archive.convertToCSV()
} importing: { data in
try ProfilesArchive(csvData: data)
}
}
}
FileRepresentation
プロフィールにビデオがある場合、ドラッグと共有もしたいですね。しかしビデオは大きくなります。メモリーにはロードしたくないですね。そこでFileRepresentation
です。
再び中身を見てみると、
FileRepresentation
は提供されるURLをレシーバーに送信し、それを使ってTransferableアイテムを再構築しています。
FileRepresentation
は、
ディスク・ファイルに書かれたバイナリー表現で共有を可能にします。
要約しますと、単純なユースケースで、単一の表現だけの場合、
まずそのモデルがCodable
に準拠していて、特定のバイナリ形式が必要か確認します。
必要であればCodableRepresentation
を使用します。
そうでなければメモリーかディスクに保存可能かチェックします。
メモリーならDataRepresentation
、ディスクならFileRepresentation
です。
Advanced tips and tricks
Transferableはシンプルな使用だけでなく、複雑なものにも対応し、ほとんどの場合わずか数行です。見てください!
ProxyRepresentation
Profileは
Transferableに準拠していますが、さらに
Profile`がコピーされ、テキスト欄にペーストされる場合、プロフィールの名前を貼りたいですよね。
もう一つの表現を加える必要があります。
ProxyRepresentation
は他のTransferable
で我々のモデルを表現することを可能にします。
Profile
は1行のテキストで貼りつけられます。
ProxyRepresentation
はCodable
の後に追加しました。順番は重要です。
レシーバーは対応するコンテンツタイプの最初の表現を使います。
レシーバーがカスタムコンテンツのプロフィールタイプを知っている場合は、これを使用します。
テキストをサポートしていない場合は、代わりにProxyRepresentation
を使用します。
import CoreTransferable
import UniformTypeIdentifiers
struct Profile: Codable {
var id: UUID
var name: String
var bio: String
var funFacts: [String]
var video: URL?
var portrait: URL?
}
extension Profile: Transferable {
static var transferRepresentation: some TransferRepresentation {
CodableRepresentation(contentType: .profile)
ProxyRepresentation(exporting: \.name)
}
}
// also declare the content type in the Info.plist
extension UTType {
static var profile: UTType = UTType(exportedAs: "com.example.profile")
}
これでProfileはエンコーダとデコーダ両方の変換と、
テキスト変換をサポートします。
ProxyRepresentationはこの場合、テキストへのエクスポートのみを記述し、プロフィールを再構築することはしません。
どんな表現も、両方の変換か、あるいは1つだけを記述できます。
Proxy and file representations
ProxyRepresentations
をご説明しましたが、ビデオにFileRepresentation
は必要でしょうか?
URLでプロキシを利用できます。違いはわずかです。
FileRepresentation
は
ディスクに書かれたURLを意図しており、一時的なサンドボックス拡張を付与することで、このファイルあるいはそのコピーへのレシーバーのアクセスを保証します。
ProxyRepresentation
はURLを同じように扱います。
ファイルに必要な追加機能はありません。つまり両方を持つことができます。
最初はFileRepresentationで、レシーバーがこのコンテンツの映画のファイルにアクセスできるようにします。
2番目のものはコピーしたビデオをテキストフィールドにペーストすると動作します。
import CoreTransferable
struct Video: Transferable {
let file: URL
static var transferRepresentation: some TransferRepresentation {
FileRepresentation(contentType: .mpeg4Movie) { SentTransferredFile($0.file) }
importing: { received in
let copy = try Self.copyVideoFile(source: received.file)
return Self.init(file: copy) }
ProxyRepresentation(exporting: \.file)
}
static func copyVideoFile(source: URL) throws -> URL {
let moviesDirectory = try FileManager.default.url(
for: .moviesDirectory, in: .userDomainMask,
appropriateFor: nil, create: true
)
var destination = moviesDirectory.appendingPathComponent(
source.lastPathComponent, isDirectory: false)
if FileManager.default.fileExists(atPath: destination.path) {
let pathExtension = destination.pathExtension
var fileName = destination.deletingPathExtension().lastPathComponent
fileName += "_\(UUID().uuidString)"
destination = destination
.deletingLastPathComponent()
.appendingPathComponent(fileName)
.appendingPathExtension(pathExtension)
}
try FileManager.default.copyItem(at: source, to: destination)
return destination
}
}
つまりファイルとプロキシの表現で、URLの扱いが全く異なるのです。
最初のケースではペイロードはディスクのアセットです。
二番目のケースのペイロードは、リモートのWebのURL structです。
Exporting condition
もう一つバージョンアップしたいのがProfileArchive
のモデルです。CSVへの変換に対応していない場合があるので、
extension ProfilesArchive: Transferable {
static var transferRepresentation: some TransferRepresentation {
DataRepresentation(contentType: .commaSeparatedText) { archive in
try archive.convertToCSV()
} importing: { data in
try ProfilesArchive(csvData: data)
}
}
}
コードに反映させたいと思います。
見てみましょう。
CSVにエクスポートできるか、データ間で変換機能があるかBooleanのプロパティを加えます。.exportingCondition
を使います。
CSVをサポートしないアーカイブでは、このフォーマットでエクスポートできません。
コード全体
import CoreTransferable
import UniformTypeIdentifiers
struct ProfilesArchive {
var supportsCSV: Bool { true }
init(csvData: Data) throws { }
func convertToCSV() throws -> Data { Data() }
}
extension ProfilesArchive: Transferable {
static var transferRepresentation: some TransferRepresentation {
DataRepresentation(contentType: .commaSeparatedText) { archive in
try archive.convertToCSV()
} importing: { data in
try Self(csvData: data)
}
.exportingCondition { $0.supportsCSV }
}
}
このAPIを使えばSwiftUIのカスタムViewのように、カスタムのTransferRepresentation
を構築することが可能です。
唯一の要件は、他の表現を必要な方法で構成することができるbody
プロパティを提供することです。
複数の表現を組み合わせて、
再利用したい場合や、公に公開したくないプライベートなデータ表現がある場合などに便利です。
Transferableのおかげで、
私が望んでいた機能をすべて備えたこのアプリを迅速に構築することができました。
-
こちらについてはまた別記事で解説します。 ↩︎
Discussion