🏙️

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

に公開

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