iyOmSd/Title: Swift

[Swift] WWDC22 Meet Transferable

냄수 2024. 11. 13. 18:39
반응형

Meet Transferable세션을 정리한 글입니다.

 

Transferable

공유 및 데이터 전송을 위해 어떻게 모델을 직렬화하고 역직렬화하는지 설명하는 Swift 우선 선언형 방식입니다

 

Anatomy of Transferable

실행 중인 두 애플리케이션을 가정해 봅시다

그리고 사용자가 복사 및 붙여넣기 ShareSheet, 단순 드래그 또는 다른 앱 기능을 통해 한 앱에서 다른 앱으로 어떤 정보를 전달한다고 하죠

서로 다른 두 앱 간에 무언가를 보낼 때 이 모든 바이너리 데이터가 넘어갑니다

이 데이터를 보낼 때 중요한 부분은 해당 데이터가 무엇인지를 결정하는 것입니다

그건 텍스트나 영상, 가장 좋아하는 여성 엔지니어 프로필 또는 전체 아카이브일 수 있죠 그리고 데이터의 용도를 설명하는 UTType도 있고요

 

다른 앱 또는 단일 앱 내에서 공유할 수 있는 모든 유형은 두 가지 정보를 제공해야 합니다

하나는 데이터와 바이너리 데이터를 전환한 방법이며

다른 하나는 바이너리 데이터의 구조와 들어맞고 수신자에게 무엇을 얻는지 알리는 콘텐츠 유형입니다

 

콘텐츠 타입 또는 uniform 타입 식별자로 알려진 이것은 다른 바이너리 구조와 추상적인 개념의 식별자를 설명하는 Apple 고유의 기술입니다

식별자는 트리 구조를 이루며 사용자화 식별자를 정의할 수도 있습니다

 

사용자화 식별자를 선언하기 위해서는 우선 Info.plist 파일의 선언을 추가

그 다음은 코드로 선언하면 끝입니다.

import UniformTypeIdentifiers

// also declare the content type in the Info.plist
extension UTType {
    static var profile: UTType = UTType(exportedAs: "com.example.profile")
}

 

 

많은 표준 유형이 이미 Transferable을 따른다는 것입니다 문자열, 데이터, URL, 속성 문자열 이미지 등이 이에 해당하죠

 

Conforming custom types

모델에 Transferable 준수성을 추가하는 법을 알아보겠습니다

유형이 Transferable을 준수하려면 구현할 속성은 단 하나 TransferRepresentation입니다

이 속성은 어떻게 모델에 이전할 것인지를 설명합니다

주의해야 할 중요한 표현이 세 가지 있는데요 CodableRepresentation, DataRepresentation, FileRepresentation입니다

 

CodableRepresentation

struct Profile: Codable {
    var id: UUID
    var name: String
    var bio: String
    var funFacts: [String]
    var video: URL?
    var portrait: URL?
}

이런모델이 있다고할때, 이 모델은 이미 codable을 준수하고있음

때문에 CodableRepresentation을 Transferable 준수성에 포함할 수 있습니다

 

CodableRepresentation은 인코더를 사용해 프로필을 바이너리 데이터로 변환하며 디코더로 원래대로 재변환합니다

기본적으로 JSON을 사용하지만 자체 인코더 및 디코더 쌍을 사용할 수도 있습니다

 

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")
}

프로필로 다시 돌아가죠 Codable이 요구하는 단 한 가지는 필요한 콘텐츠 유형을 아는 것입니다

이는 사용자화 형식이므로 사용자화 선언형 uniform 유형 식별자를 사용합니다

프로필 콘텐츠 유형을 추가하면 다 된 겁니다

이제 프로필이 Transferable을 준수하기 때문이죠

 

DataRepresentation

이번엔 아래 모델을 한번 봅시다

struct ProfilesArchive {
  init(csvData: Data) throws
  func convertToCSV() throws -> Data
}

csv데이터로 변환을 지원하고, csv를 공유하거나 다른컴퓨터로 가져올수있는 모델입니다

데이터로 저장하거나 데이터에서 변환될 수 있으며 이는 곧 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을 준수할 수 있습니다

우리가 이미 갖고 있는 두 기능을 호출하기만 하면 됩니다

 

FileRepresentation

영상은 용량이 클 수 있기에 그걸 메모리로 불러오고 싶진 않을 겁니다

FileRepresentation은 이럴 때 필요하죠

 

제공된 URL을 수신자에게 전달하고 URL을 사용해 수신자를 위한 Transferable 항목으로 재구성

 

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
    }
}

단순 작업의 경우 단일 표현만 선택하고자 한다면 먼저 모델이 Codable을 준수하고 있고 특정 바이너리 형식 요건이 필요하지 않는지 확인합니다

해당 사항을 만족한다면 CodableRepresentation을 씁니다

아니라면, 메모리나 디스크 중 어디 저장되는지 확인합니다

전자라면 DataRepresentation을 후자라면 FileRepresetnation을 사용하는 게 가장 적합하겠죠

 

Advanced tips and tricks

이번에는 더 나아가 봅시다

프로필을 페이스트보드에 복사해서 임의의 텍스트 영역에 붙여넣을 때 프로필 이름을 붙여넣고자 합니다

이런 경우는 다른 표현이 더 필요할 겁니다

ProxyRepresentation이면 다른 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: 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")
}

ProxyRepresentation을 CodableRepresentation 뒤에 추가한 것에 주목하세요

이 순서가 중요합니다

수신자는 지원하는 콘텐츠 유형으로 첫 번째 표현을 사용합니다

사용자화 콘텐츠 유형 프로필을 수신자가 알고 있다면 수신자는 그걸 사용해야 하죠

그렇지 않고 텍스트를 지원하는 경우에는 ProxyRepresentation을 대신 사용하게 됩니다

 

이제 프로필은 인코더 및 디코더 변환 그리고 텍스트로의 변환까지 모두 지원하게 됐습니다

이 경우에 ProxyRepresentation는 텍스트를 내보내기만 할 뿐 텍스트로부터 프로필을 재구성하진 않습니다

어떤 표현이든 양쪽 변환을 하거나 한쪽 변환만 할 수 있습니다

 

이제 ProxyRepresentation을 알게 되었으니 영상을 위한 FileRepresentation이 꼭 필요할까요?

URL이 있는 프록시를 사용할 수 있는데도요 차이는 미미합니다

FileRepresentation은 디스크에 기록된 URL로 작동하며 파일 원본이나 복사본에 임시 샌드박스 확장자를 부여해 수신자의 접근을 보장합니다

ProxyRepresentation도 문자열 같은 Transferable 항목과 동일한 방식으로 URL을 처리합니다

여기엔 파일에 필요한 추가 기능이 있진 않습니다

이는 우리에게 두 표현이 전부 있다는 뜻인데요

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)
  }
}

첫 번째인 FileRepresentation은 수신자가 파일의 콘텐츠와 함께 영상 파일에 접근할 수 있게 합니다

두 번째 표현은 복사한 영상을 텍스트 필드에 붙여넣을 때 사용하죠

따라서 FileRepresentation과 ProxyRepresentation에 따라 URL은 아주 다르게 처리됩니다

첫 번째 경우에는 실제 페이로드가 디스크의 에셋입니다

두 번째 경우엔 페이로드가 원격 웹사이트로 이동할 수 있는 URL 구조로 되어 있습니다

 

 

개선하고자 하는 또 다른 모델은 ProfilesArchive입니다

struct ProfilesArchive {
    var supportsCSV: Bool { true } // << 추가
    init(csvData: Data) throws {  }
    func convertToCSV() throws -> Data { Data() }
}

CSV로 변환하는 것을 지원하지 않는 경우가 있는데 이를 코드에 반영하고자 합니다

한번 보죠 CSV로 내보내고, 데이터에서 변환 함수를 입출력할 수 있는지의 여부를 알려주는 불린 프로퍼티를 추가합니다

코드에 적용하려면 .exportingCondition을 씁니다 주어진 아카이브가 CSV를 지원하지 않는다면 그 형식으로 내보내진 않을 겁니다

반응형