写真アルバムからTransferableによって画像を取得 (iOS)

# 写真アルバムから`Transferable`によって画像を取得 (iOS)

`Transferable`に準拠した画像クラスを以下のように設計しました。自分のアプリできちんと動くように、適宜修正してください。

```swift
final class ImageTranseferable: Transferable, Sendable {
    let url: URL

    init(url: URL) {
        self.url = url
    }

    static var transferRepresentation: some TransferRepresentation {
        FileRepresentation(importedContentType: .image) { receivedTransferrable in
            if receivedTransferrable.isOriginalFile {
                return ImageTranseferable(url: receivedTransferrable.file)
            } else {
                let copy = URL.temporaryDirectory.appending(path: "\(UUID().uuidString).\(receivedTransferrable.file.pathExtension)")
                try? FileManager.default.copyItem(at: receivedTransferrable.file, to: copy)
                return ImageTranseferable(url: copy)
            }
        }
    }
}
```

`FileRepresentation`を選択した理由は、appleの[Meet Transferable](https://developer.apple.com/videos/play/wwdc2022/10062/)で紹介されている下記選択基準によって決めました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/373549/498288b1-a46a-8448-9d9c-82d20d49e2c5.png)

画像(`UIImage`)は`Codable`に準拠しておらず、オンメモリ上に存在するわけでもないので、`FileRepresentation`を選択しました。

これを使って、`UIKit`と`SwiftUI`の両方で写真を読み込むようにしていきます。

## UIKit

`PHPickerViewControllerDelegate`で取得したデータを`Transferable`によってアプリで使えるように

```swift
extension ViewController: PHPickerViewControllerDelegate {
    func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
        let itemProvider = results.first!.itemProvider
        if itemProvider.hasRepresentationConforming(toTypeIdentifier: UTType.image.identifier) {
            let _ = itemProvider.loadTransferable(type: ImageTranseferable.self, completionHandler: {
                result in
                switch result {
                case .success(let success):
                    guard let data = try? Data(contentsOf: success.url), let image = UIImage(data: data) else {
                        return
                    }
                    // 何かする。
                case .failure(let failure):
                    // 何かする。
                }
            })
        }
        picker.dismiss(animated: true)
    }
}
```

## SwiftUI

`photosPicker`で取得したデータを`Transferable`によってアプリで使えるように

```swift
struct ContentView: View {
    @State var isPresented: Bool = false
    @State var items: [PhotosPickerItem] = [] {
        didSet {
            Task {
                var _medias: [Media] = []
                for item in items {
                    let imageRepr = try? await item.loadTransferable(type: ImageTranseferable.self)
                    if let imageRepr, let data = try? Data(contentsOf: imageRepr.url), let image = UIImage(data: data) {
                        _medias.append(.init(id: UUID().uuidString, image: image))
                    }
                }
                self.medias = _medias
            }
        }
    }
    @State var medias: [Media] = []
    var body: some View {
        ScrollView {
            VStack {
                Button(action: {
                    isPresented = !isPresented
                }, label: {
                    Text("写真を開く")
                })
                ForEach(medias) {
                    media in
                    Image(uiImage: media.image!)
                        .resizable()
                        .scaledToFit()
                }
            }
        }
        .photosPicker(isPresented: $isPresented, selection: .init(get: {
            self.items
        }, set: {
            medias in
            self.items = medias
        }), matching: .any(of: [.images]), photoLibrary: .shared())
    }
}

extension ContentView {
    struct Media: Identifiable {
        let id: String
        let image: UIImage?
    }
}
```

## 終わりに

めっちゃ書き殴っているので、そのまま自分のアプリに使い回すのはやめてください🙇