ストリーミングはほとんどのブラウザと
Developerアプリで視聴できます。
-
Transferableの紹介
Transferableは、ご利用のAppでの共有、ドラッグアンドドロップ、コピー&ペーストなどの機能を簡単にサポートできるモデルレイヤープロトコルです。 一般的なユースケースにおけるAPIの使用方法や、高度な機能を活用した動作のカスタマイズ方法について解説します。また、大量のデータを処理する場合に、メモリ効率を最適化する方法も紹介します。Transferableは、モデルを拡張して文字列や画像として他のAppと共有する場合でも、カスタム宣言されたデータタイプを作成する場合でも、ご利用のAppに優れたエクスペリエンスをもたらしてくれます。
リソース
関連ビデオ
WWDC22
Tech Talks
-
ダウンロード
♪メロウなインストゥルメンタル ヒップホップ音楽♪ ♪ こんにちは Transferableセッションへようこそ SwiftUIエンジニアのJuliaです Transferableを紹介でき興奮しています ドラッグ&ドロップ コピー&ペースト その他のAppの機能をサポートする 宣言的方法について説明します SwiftUIやMacのApp開発とは別に コンピュータサイエンスにおける 女性技術者に興味がありました ヒーローを知ることは重要ですよね 女性技術者を表示追加編集できる カタログAppを作りました エンジニアや科学者のプロフィールです このAppはApp間でシームレスに 科学者のプロフィールや知識の 移動やドラッグ&ドロップを サポートすることを目指します また初めてAppで watchOSとの共有をサポートします ウォッチから パーソナリティ・プロフィールを 共有したいとの要望がありました SwiftUIによってiOSとMacでも共有できます 今年ShareSheetのデザインも一新されました すべてを可能にするには App内や 他のアプリケーションの レシーバーへの送信をサポートする モデルが必要です プロフィールのstructには 一人の人格に関する すべての情報が含まれています すべてのプロフィールをアーカイブして 友達と共有できます 技術者の情報を文字で保存し ビデオも添付できます これらのモデル・タイプをすべて共有する 新しい容易な方法があります Transferableを紹介します! これは共有やデータ転送のために モデルをシリアライズ およびデシリアライズする方法を 記述するためのSwift初の宣言的な方法です 今日は Transferableとは何か それを使うときに裏で何が起こっているのか カスタムタイプへの適合方法 最後にTransferableをカスタマイズして 必要なことを行うのに役立つ 高度なヒントやトリックについてお話しします スタートしましょう! 2つのApp が起動しており ユーザーがコピー&ペースト ShareSheet・ドラッグ または他のAppの機能を使って あるAppから別のAppに情報を渡したい場合 2つの異なるApp間で何かを送信するとき このすべてのバイナリデータが行き交います このデータを送信する際に重要なのは そのデータが何に対応しているかを 判断することです テキストやビデオや ある女性技術者のプロフィール あるいは全体のアーカイブかもしれません データが何なのか説明するUTTypeもあります Appのバイナリデータの生成を見てみましょう 他のAppあるいは同じApp内で 共有される情報をバイナリデータに あるいはその逆に 相互変換する方法と バイナリデータの構造を表す コンテントタイプが必要です コンテントのタイプ-- uniform type identifiers とは 異なるバイナリーの構造と抽象概念の 識別子について記述する アップルの独自技術です 識別子はツリーで カスタム識別子も定義できます 例えばプロフィールのバイナリデータ構造 カスタム識別子の宣言に使います 宣言をInfo.plistファイルに加え ファイル拡張子も 加えます データがディスク上にある場合 システムはこれをあなたのAppが開ける事を この情報から知ります 次にコードで宣言します コンテンツのタイプの学習には このビデオをすすめます “Uniform Type Identifierの再導入” Uniform Type Identifiersとは何で どう使うのか 明快に説明しています 良いニュースは多くの標準タイプが すでにTransferableに準拠しています 例えば 文字列・データ・URL 書式付き文字列・画像 などです 新しいSwiftUIのペーストボタンを使う 数行のコードで プロフィールにペーストしたり ビューから画像をドラッグしたり Finderや他のAppからの 画像のドロップをサポートします 新らしいShareLinkで ウォッチから共有を実装できます Transferableとは何かと その使い方の基本を説明しました Appのモデルに Transferable 準拠を 追加する方法を見てみましょう 言及したように今回のAppでは 4つのモデルタイプを共有します 文字列は既に対応しています 何も必要 ありません 1つのプロフィール プロファイルのアーカイブ およびビデオの共有ではどうでしょう Transferableに準拠させるために 実装するのはただ一つ TransferRepresentation だけです モデルがどのように転送されるか記述します 3つ重要なRepresentationがあります CodableRepresentation DataRepresentation FileRepresentation それぞれ説明します 最初は中心となるモデルのProfileの構造です id・名前・略歴・趣味と ポートレイトかビデオです 既にCodableに準拠しますので TransferableにはCodableRepresentationを 含めることができます Codableは エンコーダで プロフィールをバイナリデータへ変換し デコーダが解読します デフォルトでJSONを使用しますが 自分のエンコーダとデコーダも使用できます Codableプロトコルの詳細と エンコーダとデコーダ仕組みは このプロトコルが最初に登場したWWDC18の 「データを信頼できるものに」をご覧ください プロフィールに戻ると Codableで必要な情報は 希望のコンテンツのタイプのみです カスタム・フォーマットになるので カスタムと宣言された uniform type identifierを使います プロフィールとコンテンツタイプ を加え完了です プロフィールは Transferableに 準拠しています! 別のケースのProfilesArchiveです 既にCSVデータ変換が サポートされていますので リストを CSVファイルでエクスポーし 友達とのシェアしたり 別のマシンにインポートできます これでArchiveはDataに相互変換できますので DataRepresentationに準拠したことになります 中を覗いてみると DataRepresentationは変換関数を使って 直接バイナリ表現を作り レシーバーのために値を再構築しています DataRepresentationの使用で Transferable対応は容易です 2つの既にある関数 initializer またCSVコンバータを呼び出し 利用するだけです プロフィールにビデオがある場合 ドラッグと共有もしたいですね しかしビデオは大きくなります メモリーにはロードしたくないですね そこでFileRepresentationです 再び中身を見てみると FileRepresentationは提供されるURLを レシーバーに送信しそれを使って Transferable アイテムを再構築しています FileRepresentation は ディスク・ファイルに書かれた バイナリー表現で共有を可能にします 要約しますと 単純なユースケースで 単一の表現だけの場合 まずそのモデルがCodableに準拠していて 特定のバイナリ形式が必要か確認します 必要であれば CodableRepresentation を使用します そうでなければメモリーかディスクに 保存可能かチェックします メモリーならDataRepresentation ディスクならFileRepresetnationです Transferable はシンプルな使用だけでなく 複雑なものにも対応し ほとんどの場合わずか数行です 見てください! ProfileはTransferableに 準拠していますが さらにProfileがコピーされ テキスト欄にペーストされる場合 プロフィールの名前を貼りたいですよね もう一つの表現を加える必要があります ProxyRepresentationは他のTransferableで 我々のモデルを表現することを可能にします Profileは1行のテキストで貼りつけられます ProxyRepresentationは Codableの後に追加しました 順番は重要です レシーバーは対応するコンテンツタイプの 最初の表現を使います レシーバーがカスタムコンテンツの プロフィールタイプを知っている場合は これを使用します テキストをサポートしていない場合は 代わりにProxyRepresentationを使用します これでProfileは エンコーダとデコーダ両方の変換と テキスト変換をサポートします ProxyRepresentation はこの場合 テキストへのエクスポートのみを記述し プロフィールを再構築することはしません どんな表現も 両方の変換か あるいは1つだけを記述できます ProxyRepresentationsをご説明しましたが ビデオにFileRepresentationは 必要でしょうか? URLでプロキシを利用できます 違いはわずかです FileRepresentationは ディスクに書かれたURLを意図しており 一時的なサンドボックス拡張を付与することで このファイルあるいはそのコピーへの レシーバーのアクセスを保証します ProxyRepresentationはURLを 同じように扱います ファイルに必要な 追加機能はありません つまり両方を持つことができます 最初はFileRepresentationで レシーバーがこのコンテンツの 映画のファイルにアクセスできるようにします 2 番目のものはコピーしたビデオを テキストフィールドにペーストすると 動作します つまりファイルとプロキシの表現で URLの扱いが全く異なるのです 最初のケースでは ペイロードはディスクのアセットです 二番目のケースのペイロードは リモートのWebのURL structです もう一つバージョンアップしたいのが ProfileArchiveのモデルです CSVへの変換に対応していない場合があるので コードに反映させたいと思います 見てみましょう CSVにエクスポートできるか データ間で変換機能があるか Booleanのプロパティを加えます .exportingConditionを使います CSVをサポートしないアーカイブでは このフォーマットでエクスポートできません このAPIを使えば SwiftUIのカスタムViewのように カスタムのTransferRepresentationを 構築することが可能です 唯一の要件は 他の表現を必要な方法で構成することができる bodyプロパティを提供することです 複数の表現を組み合わせて 再利用したい場合や 公に公開したくないプライベートな データ表現がある場合などに便利です Transferableのおかげで 私が望んでいた機能を すべて備えたこのAppを 迅速に構築することができました ありがとうございました 素晴らしいAppを作っていきましょう!
-
-
4:36 - Declaring a custom content type
import UniformTypeIdentifiers // also declare the content type in the Info.plist extension UTType { static var profile: UTType = UTType(exportedAs: "com.example.profile") }
-
5:10 - PasteButton interface
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) } } }
-
5:19 - Drag and Drop
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 } } }
-
5:27 - Sharing
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)) } } }
-
6:34 - Profile structure
import Foundation struct Profile: Codable { var id: UUID var name: String var bio: String var funFacts: [String] var video: URL? var portrait: URL? }
-
7:31 - CodableRepresentation
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") }
-
8:30 - DataRepresentation
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) } } }
-
9:14 - FileRepresentation
import CoreTransferable struct Video: Transferable { let file: URL static var transferRepresentation: some TransferRepresentation { FileRepresentation(contentType: .mpeg4Movie) { SentTransferredFile($0.file) } importing: { received in let destination = try Self.copyVideoFile(source: received.file) return Self.init(file: destination) } } 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 } }
-
10:05 - 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") }
-
11:34 - Proxy and file representations
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 } }
-
12:57 - Exporting condition
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 } } }
-
-
特定のトピックをお探しの場合は、上にトピックを入力すると、関連するトピックにすばやく移動できます。
クエリの送信中にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。