[SwiftUI] SwiftUI から PHPicker を使用する方法

SwiftUI

SwiftUI から PHPickerViewController を使用する方法を説明します

UIImagePickerController

WWDC2020 の"Meet the new Photos picker"をみると、UIImagePickerController を PHPicker に切り替えることを勧めているようです。

ドキュメントを確認しても、まだ、Decprecated にはなっていませんが、強い理由がなければ、今後の開発には PHPicker を使用する方が良いようです。

MEMO
UIImagePickerController の定義を確認すると複数の場所に以下のような記載があります。
soft deprecation

@available(iOS, introduced: 11.0, deprecated: 100000, message: "Will be removed in a future release, use PHPicker.")
# 11.0 は付与されているプロパティやメソッドにより異なります

deprecated になるバージョン(100000)は、非常に将来のバージョンを指していますが、まだ決まっていないためで、そう遠くない将来に deprecated になりそうです。

SwiftUI[SwiftUI] UIImagePickerController を SwiftUI から使う方法

PHPickerViewController

Apple のドキュメントは、こちら

UIImagePickerController からの違いは以下です。

  • アクセスすることに対して、ユーザーの許諾を得る必要はない(WWDC2020 のビデオでは、許諾を得ようとするな と言ってます)
  • PHPickerViewController 内で カメラを使って撮影して、その写真を使用することはできない
  • UIImage 等のデータを取得するために、ItemProvider が返される
  • PHAsset にアクセスしたければ、渡された ID から fetch しないといけない (ユーザーからの許諾が必要となるはずです)

PHPicker を使う

PHPickerViewController は、初期化時に、 PHPickerConfiguration を使用して、その動作を決める必要があります。

写真等を選択された後の動作を決めるために、delegate(PHPickerViewControllerDelegate) を指定できるようになっています。

Delegate のメソッドに渡される要素は、PHPickerResult の配列です。

MEMO
UIImagePickerController では、UIImagePickerController.infoKey をキー値とする Dictionary でした

PHPickerConfiguration

PHPickerConfiguration を使用して、選択対象等を設定します。

Apple のドキュメントは、こちら

filter
.image や .video を指定することで、選択対象をフィルターすることができます
selectionLimit
要素の選択数を指定できます。デフォルトは1です。0を指定すると、複数(システムが許す最大数)指定となります。
preferredAssetRepresentationMode
automatic, compatible, current から選べますが、その意味の説明は、Apple のドキュメントには記載されていません。デフォルトは、automatic です。

PHPickerViewControllerDelegate

要素選択終了時/キャンセル時 に PHPickerViewController に設定された delegate のメソッドが呼ばれます。

Apple のドキュメントは、こちら

PHPickerViewControllerDelegate が要求するメソッドは、以下の1つです。

PHPickerViewControllerDelegate protocol

func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult])

キャンセルされた時は、results が 空配列になって呼び出されます。

PHPickerResult

選択された要素の情報が保持されています。

Apple のドキュメントは、こちら

以下のプロパティを持っています。

assetIdentifier
必要であれば、この ID を使って、PHAsset を取得します
itemProvider
通常は、このプロパティ経由でデータを取得します
itemProvider
D&D でも使われている ItemProvider です。

ItemProvider から、UIImage 取得するコード例

if itemProvider.canLoadObject(ofClass: UIImage.self) {
  itemProvider.loadObject(ofClass: UIImage.self) { image, error in
    if let image = image as? UIImage {
      // process image here 
    }
    if let error = error {
      // error ?
    }
  }
}

PHPicker を SwiftUI で使えるようにする

UIViewControllerRepresentable で wrap する

PHPickerViewController は、ViewController なので、SwiftUI で使うためには、UIViewController Representable で wrap します

使用する時に、初期化用の PHPickerConfiguration と PHPickerViewControllerDelegate で呼び出される closure を指定するようにしました。

SwiftUIPHPicker code

//
//  SwiftUIPHPicker.swift
//
//  Created by : Tomoaki Yagishita on 2020/12/07
//  © 2020  SmallDeskSoftware
//

import Foundation
import SwiftUI
import PhotosUI
import os

public typealias PHPickerViewCompletionHandler = ( ([PHPickerResult]) -> Void)

public struct SwiftUIPHPicker: UIViewControllerRepresentable {
    // 1
    var configuration: PHPickerConfiguration
    // 2
    var completionHandler: PHPickerViewCompletionHandler?
    
    let logger = Logger(subsystem: "com.smalldesksoftware.SwiftUIPHPicker", category: "SwiftUIPHPicker")
    
    public init(configuration: PHPickerConfiguration, completion: PHPickerViewCompletionHandler? = nil) {
        self.configuration = configuration
        self.completionHandler = completion
    }
    
    public func makeCoordinator() -> Coordinator {
        logger.debug("makeCoordinator called")
        return Coordinator(self)
    }
    //(3)
    public func makeUIViewController(context: Context) -> PHPickerViewController {
        logger.debug("makeUIViewController called")
        let viewController = PHPickerViewController(configuration: configuration)
        viewController.delegate = context.coordinator
        return viewController
    }
    
    //(4)
    public func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {
        logger.debug("updateUIViewController called")
    }
    
    
    public class Coordinator : PHPickerViewControllerDelegate {
        let parent: SwiftUIPHPicker
        
        init(_ parent: SwiftUIPHPicker) {
            self.parent = parent
        }
        //(5)
        public func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
            parent.logger.debug("didFinishPicking called")
            picker.dismiss(animated: true)
            // (6)
            self.parent.completionHandler?(results)
        }
    }

}
コード解説
  1. PHPickerConfiguration は、外部から受け取ったものを使います
  2. completionHandler は、([PHPickerResult]) -> Void の closure で、delegate が呼び出された時に呼ばれます。空であれば、PHPicker を閉じるだけです
  3. PHPickerViewController を生成した時に、Coordinator を delegate に設定します
  4. updateUIViewController は、現時点では空実装です
  5. delegate メソッドは、Coordinator に実装しています
  6. delegate が呼ばれた時に、初期化時に指定した closure を呼び出します

サンプルコード

以下が、SwiftUIPHPicker (PHPickerViewController を wrap したもの)を使うコードの例です。

SwiftUIPHPicker example

//
//  ContentView.swift
//
//  Created by : Tomoaki Yagishita on 2020/12/07
//  © 2020  SmallDeskSoftware
//

import SwiftUI
import PhotosUI
import os
import SwiftUIPHPicker

struct ContentView: View {
    // (1)
    @State private var images:[UIImage] = []
    // (2)
    @State private var showPHPicker:Bool = false
    // (3)
    static var config: PHPickerConfiguration {
        var config = PHPickerConfiguration()
        config.filter = .images
        return config
    }
    let logger = Logger(subsystem: "com.smalldesksoftware.PHPickerSample", category: "PHPickerSample")

    var columns: [GridItem] = Array(repeating: .init(.fixed(100)), count: 3)
    var body: some View {
        VStack {
            HStack {
                Spacer()
                Button(action: {
                    showPHPicker.toggle()
                }, label: {
                    Image(systemName: "plus")
                        .font(.largeTitle)
                })
            }
            .padding()
            Spacer()
            LazyVGrid(columns: columns) {
                ForEach(images, id: \.self) { image in
                    Image(uiImage: image)
                        .resizable().scaledToFit()
                }
            }
            .padding()
            Spacer()
        }
        .sheet(isPresented: $showPHPicker) {
            // (4)
            SwiftUIPHPicker(configuration: ContentView.config) { results in
                for result in results {
                    let itemProvider = result.itemProvider
                    if itemProvider.canLoadObject(ofClass: UIImage.self) {
                        itemProvider.loadObject(ofClass: UIImage.self) { image, error in
                            if let image = image as? UIImage {
                                DispatchQueue.main.async {
                                    // (5)
                                    self.images.append(image)
                                }
                            }
                            if let error = error {
                                logger.error("\(error.localizedDescription)")
                            }
                        }
                    }
                }
            }
        }
    }
}
コード解説
  1. @State 指定した images に画像を保持します。保持された画像は、LazyVGrid を使って表示されます
  2. SwiftUIPHPicker を .sheet 設定しています。そこで使われる表示非表示フラグです。
  3. PHPickerViewController で使用される configuration です。イメージを1つ 選択する設定です。"config.selectionLimit = 0" と追加すると複数枚選択になります。
  4. SwiftUIPHPicker は Sheet として表示されるように設定します
  5. itemProvider から UIImage が取れた時は、 images に追加していきます。(@State 変数なので、UI の更新は自動に行われます)

GitHub

上記のコードを GitHub に入れてあります。SwiftPM に対応していますので、Xcode から URL を入れるだけで使えます。

まとめ:PHPickerViewController の使い方

PHPickerViewController の使い方
  • ユーザーにライブラリへのアクセス許諾をしなくても使える
  • UIImagePickerController を置き換える
  • configuration.filter で選択対象要素の種類を指定できる
  • configuration.selectionLimit で選択要素数を指定できる

説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です