🏙️

[SwiftUI]iOS 16から使えるPhotosPickerの使い方

2022/08/09に公開

PhotosPicker

PhotosPickerはUIKitで使用していたPHPickerViewController, UIImagePickerControllerのSwiftUI製のもので、iOS, macOS, watchOSというマルチプラットフォーム対応しているPhoto Pickerです。

新しい要素

  • Transferable
    ドラッグ&ドロップができることを示す新しいプロトコルです。
  • PhotosPickerItem
    ユーザーがピックした後に、渡されるアイテムです。これを使ってアプリ側で写真/動画をロードします。
    PHPickerViewControllerでいうPHPickerResult, NSItemProviderのようなものです。

https://zenn.dev/zunda_pixel/articles/3e9871a18d3b01

PhotosPicker init

PhotosPickerのinitは以下のような形です。

struct ContentView: View {
  @State var photoPickerItems: [PhotosPickerItem] = []

  var body: some View {
    PhotosPicker(
      selection: $photoPickerItems, // Bindingした[PhotosPickerItem]
      maxSelectionCount: 0, // 選択する写真の数(0で無制限)
      selectionBehavior: .ordered, // 順番が関係するか
      matching: .images, // 写真の種類を選択(nilでどれでも可に)
      preferredItemEncoding: .current, // エンコードの種類(基本currentでいいはず)
      photoLibrary: .shared()) { // ライブラリの選択
        Image(systemName: "photo")
    }
  }
}

Load Photo

Task {
  let image = try await photoPickerItem.loadTransferable(type: Image.self)
  print(image)
}

画像 Pick Sample


struct ContentView: View {
  @State var photoPickerItems: [PhotosPickerItem] = []
  @State var images: [UIImage] = []

  var body: some View {
    VStack {
      PhotosPicker(
        selection: $photoPickerItems,
        maxSelectionCount: 0,
        selectionBehavior: .ordered,
        matching: .images, // 写真の種類を画像(images)だけに
        preferredItemEncoding: .current,
        photoLibrary: .shared()) {
          Image(systemName: "photo")
        }
        .onChange(of: photoPickerItems) { newPhotoPickerItems in
          Task {
            do {
              for photoPickerItem in newPhotoPickerItems {
                if let data = try await photoPickerItem.loadTransferable(type: Data.self) {
                  if let uiImage = UIImage(data: data) {
                    images.append(uiImage)
                  }
                }
              }
            } catch {
              print(error)
            }
          }
        }
      if !images.isEmpty {
        TabView {
          ForEach(images, id: \.self) { image in
            Image(uiImage: image)
              .resizable()
              .scaledToFit()
          }
        }
        .tabViewStyle(.page(indexDisplayMode: .always))
      }
    }
    .frame(maxWidth: .infinity, maxHeight: .infinity)
    .background(.black)
  }
}

動画をPickしたい

動画をPickしたい場合新しくTransferableに準拠したものが必要です。

Transferable filerepresentation(Apple Documentation)

// サンプル実装
import CoreTransferable

struct Movie: Transferable {
  let url: URL

  static var transferRepresentation: some TransferRepresentation {
    FileRepresentation(contentType: .movie) { movie in
      SentTransferredFile(movie.url)
    } importing: { receivedData in
      let fileName = receivedData.file.lastPathComponent
      let copy: URL = FileManager.default.temporaryDirectory.appendingPathComponent(fileName)

      if FileManager.default.fileExists(atPath: copy.path) {
        try FileManager.default.removeItem(at: copy)
      }

      try FileManager.default.copyItem(at: receivedData.file, to: copy)
      return .init(url: copy)
    }
  }
}
.onChange(of: photoPickerItems) { newPhotoPickerItems in
  Task {
    do {
      for photoPickerItem in newPhotoPickerItems {
        if let movie = try await photoPickerItem.loadTransferable(type: Movie.self) {
          movies.append(movie)
        }
      }
    } catch {
      print(error)
    }
  }
}

Sample Project

iOS/ macOSで使えるSample Projectを作成しました。

watchOSはiOS/ macOSとは少し異なる挙動で、実機がないとテストができないため、対応していません。

https://github.com/zunda-pixel/SamplePhotosPicker/

Discussion