大多数浏览器和
Developer App 均支持流媒体播放。
-
Transferable 简介
了解 Transferable:它是一种模型层协议,可轻松支持 App 中的分享、拖放、复制/粘贴以及其他功能。我们将探索如何在常见场景中运用该 API,以及充分利用高级功能对行为进行自定义。我们还将分享如何在处理大量数据时对内存效率进行优化。无论是扩展您的模型,将其作为字符串或图片与其他 App 共享,或是创建自定义的声明数据类型,Transferable 都可以帮助您在 App 中打造更出色的体验。
资源
相关视频
WWDC22
Tech Talks
-
下载
♪ 柔和乐器演奏的嘻哈音乐 ♪ ♪ 大家好 欢迎观看 “Meet Transferable”讲座 我是 SwiftUI 工程师 Julia 很高兴能 向大家介绍 Transferable 一种支持在您的 App 中 拖放 复制/粘贴 及其他功能的声明方式 除了在 SwiftUI 工作 和为 Mac 开发 App 我也很想了解 在计算机行业打拼的女性 想要倾听她们的故事 我觉得英雄前辈们的 事迹十分值得我们学习 所以我决定创建一个目录 App 我可以在其中 查看 添加和编辑一份名单 该名单囊括了女性发明家 工程师 和科学家的简介资料 我希望该 App 能够完美地支持 将科学家的肖像拖放到 App 中 能够使用剪贴板内容 来粘贴有趣的事迹 并且我的 App 首次 支持在 watchOS 上分享! 我的潜在用户表示他们希望 能够从手表中 分享个人资料 此外 目前可以通过 SwiftUI 实现在 iOS 和 Mac 上的分享 这也是今年 ShareSheet 的全新设计 在背后 启用所有这些功能 需要将我们已有的模型 支持发送到一个 我们 app 里的接收方 甚至其他 App 个人资料结构包含我们所掌握的 个体的所有信息 打包在存档中的所有个人资料 可以在朋友之间分享 我们将个体有趣事迹存储在字符串中 甚至可以附加视频 这里一种新的简单方法 可以将所有这些模型类型 支持分享功能 这就是 Transferable! 这是一种 Swift 优先的声明方式 说明了如何将您的模型 序列化和反序列化 来用于分享和数据传输 今天我们就来聊聊 什么是 Transferable 以及当我们在使用时 它在幕后是怎样运作的 以及如何符合自定义类型 最后 我将分享一些高级技巧和窍门 可以帮助自定义 Transferable 来精准地满足您的需求 我们开始吧! 假设有两个 App 正在运行 用户想从一个 App 传递数据 到另一个 App 通过复制/粘贴 分享面板 拖拽 或者使用一些其它的 App 功能 当在两个不同的 App 间 发送内容时 数据将以二进制形式进行传输 发送此数据的一个重要方面 是确定其对应的内容 可以是文字 视频 我最喜欢的女工程师资料 或整个存档 这里是 描述数据用途的 UTType 让我们仔细研究 App 是 如何生成这种二进制数据的 可以与其他 App 甚至在单个 App 中 共享的所有类型 都必须提供两个信息 将其和二进制数据之间 互相转换的方法 与二进制数据结构对应的 内容类型并告知接收方 他们实际获取的数据类型 内容类型 也称为统一类型标识符 是一种 Apple 特有的技术 该技术描述不同 二进制结构以及抽象概念的标识符 该标识符形成一棵树 并且我们还可以定义自定义标识符 例如 一个用于由我们的配置文件 使用的二进制结构 要声明一个自定义标识符 首先 将其声明 添加到 Info.plist 文件中 添加一个文件扩展名 也是个不错的选择 如果数据保存在磁盘上 系统了解到您的 App 可以打开该文件 其次 在代码中声明该标识符 要了解有关内容类型的更多信息 您可以观看视频 “统一类型标识符 — 重新介绍” 我个人十分喜欢该视频 其清晰地阐述了 什么是统一类型标识符以及如何使用 好消息是许多标准类型 已符合 Transferable 例如 字符串 数据 URL 属性字符串和图像 您只需几行代码即可 使用全新 SwiftUI 粘贴按钮界面 将有趣的事迹粘贴到个人资料中 支持从视图中拖动图像 或从访达 或其他 App 接收拖进来的图像 使用全新的 ShareLink 现在可以从手表实现分享体验 介绍了基础知识后 现在您有了一个想法 什么是 Transferable 且如何使用 我们看看 如何向我们 App 中的模型 添加 遵循 Transferable 协议 正如我之前提到的 我们的 App 中有四种要分享的 模型类型 其中一个是字符串 已经遵循 Transferable 我们不需要再对其进行任何操作了 但是单个个人资料 ProfilesArchive 和我想分享的视频呢? 要使类型符合 Transferable 只需实现一个属性 TransferRepresentation 该属性描述了模型将如何被传输 需要注意三个重要的表示形式 CodableRepresentation DataRepresentation 和 FileRepresentation 我将分别介绍 但首先 先研究 我们最重要的模型 Profile 结构 其包括 ID 姓名 简历 或许还包括有趣的事迹 肖像和视频 Profile 结构 已符合 Codable 因此 我们可以将 CodableRepresentation 包含到我们的 Transferable 一致性中 Codable 表示使用编码器 将个人资料转换为二进制数据 并且解码器进行相反的转化 默认情况下 其使用 JSON 但您也可以提供 您自己的编码器/解码器对 想要了解 有关 Codable 协议 以及编码器和解码器 工作原理的更多信息 我邀请您观看首次引入此协议的 WWDC 讲座:“可信任的数据” 回到我们的个人资料 Codable 唯一需要的 就是了解所需的内容类型 由于这是一种自定义格式 所以我们将使用 自定义声明的统一类型标识符 添加个人资料内容类型后 我们就可以开始了 Profile 现在符合 Transferable 了! 现在 我们看一下另一种类型 ProfilesArchive 其已经支持转换为 CSV 数据 我可以在 CSV 文件中 导出女性个人资料列表 然后可以与朋友分享 或将其导入另一台计算机 存档可以和数据之间来回转换 这意味着我们可以用 DataRepresentation 如果我们研究的更深一点 就会发现 DataRepresentation 使用转换函数 来直接创建二进制表示形式 并重建接收器的值 这就是用 DataRepresentation 可以很容易地 符合 Transferable 只需调用我们已有的两个函数 初始化函数和转换为 CSV 的函数 如果个体资料附有视频 我还希望能将其拖动或分享 但是视频可能很大 我不想让其加载到内存中 这时候就应该用到 FileRepresentation 如果我们再次深究 我们会发现 FileRepresentation 把已提供的 URL 传递给了接收器 并用 URL 为接收器 重建了 Transferable 项 FileRepresentation 允许我们共享项目 且该项目由写入磁盘的 二进制表示形式支持 例如文件 我们总结一下 如果您只想为一个简单的用例 选择一个表示形式 首先检查模型 是否具有 Codable 一致性 并且是否没有 任何特定的二进制格式要求 如果具有一致性 便使用 CodableRepresentation 如果不具有 请检查 模型存储在内存中还是磁盘上 如果存储在内存中 那么 DataRepresentation 再适合不过了 如果在磁盘上 则是 FileRepresetnation Transferable 不仅涵盖简单的用例 还有一些复杂的用例 大多数情况下 只需几行代码便都可解决 自己去发现吧! 之前 我们向个人资料中添加了 Transferable 一致性 现在我们看得更远一些 将个人资料复制到粘贴板 并粘贴到任何文本字段中时 我想粘贴个人资料的名称 这意味着我们 需要额外添加一个表示形式 ProxyRepresentation 允许 其他 Transferable 类型 来表示我们的模型 只需一行 个人资料便可粘贴为文本 请注意 我是在 Codable 之后 添加的 ProxyRepresentation 顺序不能反 接收器将使用第一种表示形式 和其支持的内容类型 如果接收器检测到 我们的自定义内容类型 Profile 便应让其使用 如果没有检测到 但其支持文本类型 便要让其使用 ProxyRepresentation 现在 Profile 支持编码器/解码器转换 并支持转换为文本 本例中的 ProxyRepresentation 仅描述导出为文本 不从中重建配置文件 任何表示形式都 可以描述两种或一种转换 现在 我们了解了 ProxyRepresentations 我们需要对视频应用 FileRepresentation 吗? 我们可以使用带有 URL 的代理 差异很小 FileRepresentation 是用来处理 写入磁盘的 URL 并通过授予临时沙盒扩展程序 来确保接收器 可以访问此文件或其副本 ProxyRepresentation 处理 URL 的方式 与处理其他 Transferable 项一样 例如字符串 其不具备我们对文件所需的 任何这些附加功能 这意味着两个都可以使用 首先是 FileRepresentation 允许接收器访问电影文件 及其内容 当我将已复制的 视频粘贴到文本字段中时 将用到 ProxyRepresentation 因此 以上两种 处理 URL 的方式 有很大不同 在第一种情况下 实际有效负载是磁盘上的资产 第二种情况下 有效负载是可以指向 远程网站的 URL 结构本身 我还想升级另一个模型 ProfilesArchive 在某些情况下 其不支持转换为 CSV 我想在代码中体现这一点 我们一起看看 我们添加一个布尔属性 来弄清是否可以导出为 CSV 和数据间的转换函数 为了将这个想法在代码中体现 我们可以使用 .exportingCondition 如果给定的存档不支持 CSV 则存档不会以这种格式导出 使用此 API 您甚至可以构建 customTransferRepresentation 就像 SwiftUI 中的 customViews 一样 只需要提供 body 属性 您就可以在其中以您想要的方式 配置其他表示形式 如果您想重新使用表示形式的组合 或者您有一些不想公开的 私有数据表示形式 这种方法都会起到很大的作用 Transferable 帮助我 快速构建了这个 App 并拥有了我想要的所有功能 我希望它可以帮助您在最短的时间里 构建出功能最丰富的 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 } } }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。