스트리밍은 대부분의 브라우저와
Developer 앱에서 사용할 수 있습니다.
-
Transferable 소개
Transferable 소개: 앱에서 공유, 드래그 앤 드롭, 복사/붙여넣기 및 기타 기능을 손쉽게 지원할 수 있도록 하는 모델-레이어 프로토콜입니다. 일반적인 사용 사례에서 API를 사용하는 방법을 알아보고, 고급 기능을 활용하여 동작을 맞춤화하는 방법을 알아보겠습니다. 또한 많은 양의 데이터를 다룰 때 메모리 효율성을 목표로 최적화할 방법을 공유합니다. 모델을 문자열이나 이미지로 다른 응용 프로그램과 공유하도록 확장하든, 맞춤형으로 선언된 데이터 타입을 생성하든 관계없이 Transferable을 사용하면 앱에서 뛰어난 경험을 조성할 수 있습니다.
리소스
관련 비디오
WWDC22
Tech Talks
-
다운로드
♪ 부드러운 힙합 음악 ♪ ♪ 안녕하세요, "Transferable 소개” 세션에 오신 것을 환영합니다 제 이름은 Julia고 SwiftUI 엔지니어입니다 앱에 드래그 앤 드롭과 복사 및 붙여넣기 그리고 기타 다른 기능을 지원하는 선언형 방식인 Transferable을 소개할 수 있어서 정말 기쁩니다 저는 SwiftUI와 Mac용 애플리케이션 개발 외에도 컴퓨터 과학에 종사하는 여성들의 이야기에도 관심이 많습니다 우리의 영웅을 아는 것이 중요하다고 생각하거든요 그래서 여성 발명가와 엔지니어 과학자들의 프로필을 보고, 추가하고, 편집할 수 있는 카탈로그 애플리케이션을 만들기로 했죠 이 애플리케이션이 끊김 없는 드래그 앤 드롭으로 과학자들의 사진을 앱으로 옮기는 것을 지원하고 흥미로운 내용을 클립보드를 통해 붙여넣기할 수 있길 원했습니다 그리고 처음으로 제 앱이 watchOS에서도 공유하기를 지원하게 됐어요! 제 잠재적인 사용자들은 Apple Watch로 성격 프로필을 공유할 수 있기를 원한다고 해서요 또한 올해 ShareSheet용으로 완전히 새롭게 설계된 공유하기가 SwiftUI를 통해 iOS와 Mac에서도 가능해졌습니다 내부적으로 이 모든 기능을 가능하게 하려면 이미 지원해야 하는 모델을 앱이나 다른 프로그램 내부의 수신자에게 전송해야 합니다 프로필 구조에는 단일 성격의 모든 정보가 포함되어 있습니다 아카이브에 저장된 모든 프로필은 친구 간에 공유할 수 있죠 또 성격에 관한 재밌는 사실을 문자열에 저장하고 영상을 첨부할 수도 있습니다 그리고 이 모든 모델 유형을 공유할 수 있는 새롭고 쉬운 방법이 있습니다 바로 Transferable이죠! 공유 및 데이터 전송을 위해 어떻게 모델을 직렬화하고 역직렬화하는지 설명하는 Swift 우선 선언형 방식입니다 오늘 소개드릴 내용은 Transferable은 무엇이며 이걸 사용했을 때 씬에서 일어나는 일 사용자화 유형을 준수하는 법 그리고 마지막으로 Transferable을 사용자화해서 필요한 작업을 정확히 수행하는 데 도움이 되는 고급 팁과 요령입니다 그럼 시작하죠! 실행 중인 두 애플리케이션을 가정해 봅시다 그리고 사용자가 복사 및 붙여넣기 ShareSheet, 단순 드래그 또는 다른 앱 기능을 통해 한 앱에서 다른 앱으로 어떤 정보를 전달한다고 하죠 서로 다른 두 앱 간에 무언가를 보낼 때 이 모든 바이너리 데이터가 넘어갑니다 이 데이터를 보낼 때 중요한 부분은 해당 데이터가 무엇인지를 결정하는 것입니다 그건 텍스트나 영상 가장 좋아하는 여성 엔지니어 프로필 또는 전체 아카이브일 수 있죠 그리고 데이터의 용도를 설명하는 UTType도 있고요 앱이 이 바이너리 데이터를 어떻게 생성하는지 자세히 봅시다 다른 앱 또는 단일 앱 내에서 공유할 수 있는 모든 유형은 두 가지 정보를 제공해야 합니다 하나는 데이터와 바이너리 데이터를 전환한 방법이며 다른 하나는 바이너리 데이터의 구조와 들어맞고 수신자에게 무엇을 얻는지 알리는 콘텐츠 유형입니다 콘텐츠 유형 또는 균일 유형 식별자로 알려진 이것은 다른 바이너리 구조와 추상적인 개념의 식별자를 설명하는 Apple 고유의 기술입니다 식별자는 트리 구조를 이루며 사용자화 식별자를 정의할 수도 있습니다 프로필에서 사용하는 바이너리 구조가 그 예시에 해당하죠 사용자화 식별자를 선언하기 위해서는 우선 Info.plist 파일의 선언을 추가해야 합니다 파일 확장자를 추가하는 것도 좋은 생각이죠 데이터가 디스크에 저장되면 앱이 그 파일을 열 수 있음을 시스템이 알게 됩니다 그 다음은 코드로 선언해야 합니다 콘텐츠 유형에 관해 더 자세히 알고 싶으시다면 "Uniform Type Identifiers -- A reintroduction"이라는 영상을 보시길 바랍니다 제가 개인적으로 좋아하는 영상이며 균일 유형 식별자가 무엇이고 어떻게 쓰는지 확실히 알 수 있죠 좋은 소식은 많은 표준 유형이 이미 Transferable을 따른다는 것입니다 문자열, 데이터, URL, 속성 문자열 이미지 등이 이에 해당하죠 새로운 SwiftUI 붙여넣기 버튼 인터페이스로 재밌는 사실을 프로필에 붙여넣으려면 코드 몇 줄만 있으면 됩니다 뷰에서 이미지를 드래그해 Finder나 다른 앱에서 드롭하거나 받는 것도 지원합니다 최신 ShareLink를 사용하면 Apple Watch에서도 공유 경험을 구현할 수 있죠 지금까지 기초를 다루었으니 이제 Transferable이 무엇이며 또 어떻게 쓰는지 아셨을 겁니다 이제 애플리케이션 모델에 Transferable 준수성을 추가하는 법을 알아보겠습니다 앞에서 언급했듯이 앱에서는 네 가지 모델 유형을 공유할 수 있습니다 그중에 문자열은 이미 Transferable을 준수하기에 추가로 더 할 필요는 없습니다 하지만 단일 프로필이나 ProfilesArchive 영상을 공유하려면 어떻게 해야 할까요? 유형이 Transferable을 준수하려면 구현할 속성은 단 하나 TransferRepresentation입니다 이 속성은 어떻게 모델에 이전할 것인지를 설명합니다 주의해야 할 중요한 표현이 세 가지 있는데요 CodableRepresentation DataRepresentation FileRepresentation입니다 각각 다뤄 볼 텐데요 하지만 먼저, 저희의 핵심 모델인 프로필 구조를 만나 보시죠 이 안에는 아이디, 이름, 약력 혹은 몇 가지 재밌는 사실 사진, 영상이 있습니다 코딩 가능성을 이미 준수하죠 때문에 CodableRepresentation을 Transferable 준수성에 포함할 수 있습니다 CodableRepresentation은 인코더를 사용해 프로필을 바이너리 데이터로 변환하며 디코더로 원래대로 재변환합니다 기본적으로 JSON을 사용하지만 자체 인코더 및 디코더 쌍을 사용할 수도 있습니다 코딩 가능 프로토콜과 인코더 및 디코더의 작동 기전을 더 자세히 알고 싶으시다면 이 프로토콜을 처음 소개한 WWDC 세션인 "Data you can trust"를 보시기 바랍니다 우리 프로필로 다시 돌아가죠 코딩 가능성이 요구하는 단 한 가지는 필요한 콘텐츠 유형을 아는 것입니다 이는 사용자화 형식이므로 사용자화 선언형 균일 유형 식별자를 사용합니다 프로필 콘텐츠 유형을 추가하면 다 된 겁니다 이제 프로필이 Transferable을 준수하기 때문이죠! 이제 ProfilesArchive를 살펴보도록 하죠 이미 CSV 데이터로의 변환을 지원하고 있는데요 그렇기에 여성 프로필 목록을 CSV 파일로 내보낸 다음 친구들과 공유하거나 다른 컴퓨터로 가져올 수 있죠 아카이브는 데이터로 저장하거나 데이터에서 변환될 수 있으며 이는 곧 DataRepresentation을 사용할 수 있음을 의미합니다 내부를 들여다보면 DataRepresentation이 변환 함수를 사용하여 직접 바이너리 표현을 만들고 수신자를 위한 값을 재구성하는 것이 보이실 겁니다 따라서 DataRepresentation을 사용하면 아주 간단하게 Transferable을 준수할 수 있습니다 우리가 이미 갖고 있는 두 기능을 호출하기만 하면 됩니다 식별자와 CSV로의 변환기죠 성격 프로필에 영상이 첨부되어 있다면 그것 역시 드래그하거나 공유하고 싶겠죠 하지만 영상은 용량이 클 수 있기에 그걸 메모리로 불러오고 싶진 않을 겁니다 FileRepresentation은 이럴 때 필요하죠 이번에도 장막을 들춰 보면 FileRepresentation이 제공된 URL을 수신자에게 전달하고 URL을 사용해 수신자를 위한 Transferable 항목으로 재구성합니다 FileRepresentation을 사용하면 디스크에 기록된 바이너리 표현을 원래의 파일로 다시 복원해 해당 항목을 공유할 수 있습니다 정리해 보죠 단순 작업의 경우 단일 표현만 선택하고자 한다면 먼저 모델이 코딩 가능 준수성을 갖고 있고 특정 바이너리 형식 요건이 필요하지 않는지 확인합니다 해당 사항을 만족한다면 CodableRepresentation을 씁니다 아니라면, 메모리나 디스크 중 어디 저장되는지 확인합니다 전자라면 DataRepresentation을 후자라면 FileRepresetnation을 사용하는 게 가장 적합하겠죠 Transferable는 단순 작업 외에도 복잡한 작업에도 쓸 수 있습니다 그리고 대부분의 경우 코드 몇 줄만 있으면 되죠 직접 확인하세요! 이전에는 Transferable 준수성만 프로필에 추가했습니다만 이번에는 더 나아가 봅시다 프로필을 페이스트보드에 복사해서 임의의 텍스트 영역에 붙여넣을 때 프로필 이름을 붙여넣고자 합니다 이런 경우는 다른 표현이 더 필요할 겁니다 ProxyRepresentation이면 다른 Transferable 유형이 현재 모델에 나타날 수 있습니다 딱 한 줄로 프로필이 텍스트로 붙여넣어지죠 ProxyRepresentation을 CodableRepresentation 뒤에 추가한 것에 주목하세요 이 순서가 중요합니다 수신자는 지원하는 콘텐츠 유형으로 첫 번째 표현을 사용합니다 사용자화 콘텐츠 유형 프로필을 수신자가 알고 있다면 수신자는 그걸 사용해야 하죠 그렇지 않고 텍스트를 지원하는 경우에는 ProxyRepresentation을 대신 사용하게 됩니다 이제 프로필은 인코더 및 디코더 변환 그리고 텍스트로의 변환까지 모두 지원하게 됐습니다 이 경우에 ProxyRepresentation는 텍스트를 내보내기만 할 뿐 텍스트로부터 프로필을 재구성하진 않습니다 어떤 표현이든 양쪽 변환을 하거나 한쪽 변환만 할 수 있습니다 이제 ProxyRepresentation을 알게 되었으니 영상을 위한 FileRepresentation이 꼭 필요할까요? URL이 있는 프록시를 사용할 수 있는데도요 차이는 미미합니다 FileRepresentation은 디스크에 기록된 URL로 작동하며 파일 원본이나 복사본에 임시 샌드박스 확장자를 부여해 수신자의 접근을 보장합니다 ProxyRepresentation도 문자열 같은 Transferable 항목과 동일한 방식으로 URL을 처리합니다 여기엔 파일에 필요한 추가 기능이 있진 않습니다 이는 우리에게 두 표현이 전부 있다는 뜻인데요 첫 번째인 FileRepresentation은 수신자가 파일의 콘텐츠와 함께 영상 파일에 접근할 수 있게 합니다 두 번째 표현은 복사한 영상을 텍스트 필드에 붙여넣을 때 사용하죠 따라서 FileRepresentation과 ProxyRepresentation에 따라 URL은 아주 다르게 처리됩니다 첫 번째 경우에는 실제 페이로드가 디스크의 에셋입니다 그리고 두 번째 경우엔 페이로드가 원격 웹사이트로 이동할 수 있는 URL 구조로 되어 있습니다 개선하고자 하는 또 다른 모델은 ProfilesArchive입니다 CSV로 변환하는 것을 지원하지 않는 경우가 있는데 이를 코드에 반영하고자 합니다 한번 보죠 CSV로 내보내고, 데이터에서 변환 함수를 입출력할 수 있는지의 여부를 알려주는 불린 프로퍼티를 추가합니다 이 발상을 코드에 적용하려면 .exportingCondition을 씁니다 주어진 아카이브가 CSV를 지원하지 않는다면 그 형식으로 내보내진 않을 겁니다 이 API를 사용한다면 SwiftUI의 customViews처럼 customTransferRepresentation을 구축할 수도 있습니다 유일한 요건은 필요한 방식으로 다른 표현을 구성하는 본문 프로퍼티를 제공하는 것입니다 이 API는 표현의 조합을 재사용하거나 공개 노출을 피해야 하는 개인 데이터 표현이 있는 경우에 유용합니다 제가 원하는 모든 기능을 갖춘 애플리케이션을 만들 때 Transferable에게 많은 도움을 받았습니다 여러분도 그 어느 때보다 짧은 시간 안에 기능이 풍부한 앱을 만들 때 도움을 받길 바랍니다 이 세션에 참여해 주셔서 감사드리며 계속해서 놀라운 앱을 만드시길 바랄게요! ♪
-
-
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 } } }
-
-
찾고 계신 콘텐츠가 있나요? 위에 주제를 입력하고 원하는 내용을 바로 검색해 보세요.
쿼리를 제출하는 중에 오류가 발생했습니다. 인터넷 연결을 확인하고 다시 시도해 주세요.