【SwiftUI】UIImagePickerControllerとPHPickerViewControllerを使ってカメラとフォトライブラリから動画を取得する

2022.05.06

SwiftUIで端末のカメラで撮影した動画、またはフォトライブラリにある動画を取得したかった為、調べました。

以前からUIImagePickerControllerにはお世話になっていましたが、UIImagePickerController.SourceType.photoLibraryはiOS 14より新しいOSバージョンでは非推奨になっている為、フォトライブラリからソースを取り出す場合はPHPickerViewControllerの使用が推奨されております。

なので今回はカメラから動画を取得する場合は、UIImagePickerControllerを使用して、フォトアルバムから動画を取得する場合は、PHPickerViewControllerを使用する方法を調べてみました。

環境

  • Xcode 13.3
  • iOS 15.4.1

作ったもの

カメラで撮影した動画、またはフォトライブラリに保存してある動画を取得してVideoPlayerで再生するアプリです。

swiftui-image-picker-phpicker-demo

カメラ撮影した動画を取得する

カメラで撮影した動画を取得する為に、UIImagePickerControllerを使用します。SwiftUIで使用する為にUIViewControllerRepresentableに準拠させたstructを作成します。

import UniformTypeIdentifiers
import SwiftUI

struct CameraMoviePickerView: UIViewControllerRepresentable {

    @Environment(\.dismiss) private var dismiss
    @Binding var movieUrl: URL?

    func makeUIViewController(context: Context) -> UIImagePickerController {

        let picker = UIImagePickerController()
        picker.sourceType = .camera

        picker.delegate = context.coordinator
        picker.mediaTypes = [UTType.movie.identifier]
        picker.videoQuality = .typeHigh

        return picker
    }

    func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {}

    func makeCoordinator() -> Coordinator {
        return Coordinator(self)
    }

    class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {

        let parent: CameraMoviePickerView

        init(_ parent: CameraMoviePickerView) {
            self.parent = parent
        }

        func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {

            guard let movieURL = info[UIImagePickerController.InfoKey.mediaURL] as? URL else {
                return
            }

            parent.movieUrl = movieURL
            parent.dismiss()
        }

        func imagePickerControllerDidCancel(_: UIImagePickerController) {
            parent.dismiss()
        }
    }
}

内容については説明していきます。

プロパティ

ImagePickerで動画を取得、または取得をキャンセルした場合にViewを破棄する為に環境変数dismissを用意します。

@Environment(\.dismiss) private var dismiss

動画のURLを取得した時にバインディングしたいのでバインディング変数を定義します。

@Binding var movieUrl: URL?

makeUIViewController

makeUIViewControllerで今回生成するUIImagePickerControllerの設定を行い、返り値として渡します。

func makeUIViewController(context: Context) -> UIImagePickerController {

    let picker = UIImagePickerController()
    picker.sourceType = .camera
    picker.mediaTypes = [UTType.movie.identifier]
    picker.videoQuality = .typeHigh
    picker.delegate = context.coordinator

    return picker
}
  • sourceType
    • 今回はカメラからの動画を取得するので.cameraを指定しています。
  • mediaTypes
    • 今回はメディアは動画を取得したいので、[UTType.movie.identifier]を指定します。
  • videoQuality
    • 特に指定はないですが、今回は高品質動画.typeHighにしています。
  • delegate
    • UIViewControllerRepresentableでdelegateメソッドを呼ぶ為に、context.coordinatorを代入しています。

makeCoordinator

ViewControllerのイベントをSwiftUIに伝達する役割を果たすCoordinatorを生成します。

func makeCoordinator() -> Coordinator {
    return Coordinator(self)
}

Coordinator

UIImagePickerControllerDelegateを使用する為にUINavigationControllerDelegateUIImagePickerControllerDelegateに準拠しています。

class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {

    let parent: CameraMoviePickerView

    init(_ parent: CameraMoviePickerView) {
        self.parent = parent
    }

    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {

        guard let movieURL = info[UIImagePickerController.InfoKey.mediaURL] as? URL else {
            return
        }

        parent.movieUrl = movieURL
        parent.dismiss()
    }

    func imagePickerControllerDidCancel(_: UIImagePickerController) {
        parent.dismiss()
    }
}

parent

UIImagePickerControllerのイベント伝達を受け取る変数になります。

let parent: CameraMoviePickerView

imagePickerController(_:, didFinishPickingMediaWithInfo:)

UIImagePickerControllerでメディア(今回は動画)が選択された時に呼ばれるメソッドです。

func imagePickerController(_ picker: UIImagePickerController,
                           didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {

    guard let movieURL = info[UIImagePickerController.InfoKey.mediaURL] as? URL else {
        return
    }

    parent.movieUrl = movieURL
    parent.dismiss()
}

選択された動画のURLを取得して、CameraMoviePickermovieUrlに代入しています。その後、dismissを行い、画面を閉じるようにしています。

imagePickerControllerDidCancel(_:)

UIImagePickerController のキャンセルボタンが押された時に呼ばれるメソッドなので、dismissを行い、画面を閉じています。

func imagePickerControllerDidCancel(_: UIImagePickerController) {
    parent.dismiss()
}

これでUIImagePickerControllerを使用してカメラで撮影した動画を取得出来ました。

フォトライブラリから動画を取得する

冒頭で説明した通り、iOS 14.0より新しいバージョンでは、UIImagePickerController.SourceType.photoLibraryは非推奨になっている為、フォトアルバムの動画を取得するのにPHPickerViewControllerを使用します。

PHPickerViewControllerUIImagePickerControllerの代替えクラスで、安定性と信頼性が向上しています。開発者とユーザーは複数の恩恵を受けることが出来るそうです。詳細はPHPickerViewController をご覧下さい。

import SwiftUI
import PhotosUI

struct PhotoLibraryMoviePickerView: UIViewControllerRepresentable {

    @Environment(\.dismiss) private var dismiss
    @Binding var movieUrl: URL?

    func makeUIViewController(context: Context) -> PHPickerViewController {
        var configuration = PHPickerConfiguration()
        configuration.filter = .videos
        configuration.preferredAssetRepresentationMode = .current
        let picker = PHPickerViewController(configuration: configuration)
        picker.delegate = context.coordinator
        return picker
    }

    func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {}

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    class Coordinator: NSObject, PHPickerViewControllerDelegate {

        let parent: PhotoLibraryMoviePickerView

        init(_ parent: PhotoLibraryMoviePickerView) {
            self.parent = parent
        }

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

            parent.dismiss()

            guard let provider = results.first?.itemProvider else {
                return
            }

            let typeIdentifier = UTType.movie.identifier

            if provider.hasItemConformingToTypeIdentifier(typeIdentifier) {

                provider.loadFileRepresentation(forTypeIdentifier: typeIdentifier) { url, error in
                    if let error = error {
                        print("error: \(error)")
                        return
                    }
                    if let url = url {
                        let fileName = "\(Int(Date().timeIntervalSince1970)).\(url.pathExtension)"
                        let newUrl = URL(fileURLWithPath: NSTemporaryDirectory() + fileName)
                        try? FileManager.default.copyItem(at: url, to: newUrl)
                        self.parent.movieUrl = newUrl
                    }
                }
            }
        }
    }
}

内容について説明していきます。

プロパティ

PHPickerViewControllerで動画を取得、または取得をキャンセルした場合にViewを破棄する為に環境変数dismissを用意します。

@Environment(\.dismiss) private var dismiss

動画のURLを取得した時にバインディングしたいのでバインディング変数を定義します。

@Binding var movieUrl: URL?

makeUIViewController

makeUIViewControllerで今回生成するPHPickerViewControllerの設定を行い、返り値として渡します。

PHPickerViewController のインスタンス生成にはPHPickerConfigurationが必要なので、まずはPHPickerConfigurationの設定を行い、その構成と共にPHPickerViewControllerを生成します。

func makeUIViewController(context: Context) -> PHPickerViewController {
    var configuration = PHPickerConfiguration()
    configuration.filter = .videos
    configuration.preferredAssetRepresentationMode = .current
    let picker = PHPickerViewController(configuration: configuration)
    picker.delegate = context.coordinator
    return picker
}
  • configuration.filter
    • ピッカーがどのアセットタイプを表示させるかのフィルタリングです。今回は動画なので.videoを指定しています。
  • configuration.preferredAssetRepresentationMode
    • アセットに複数の表現が含まれている場合に使用する表現を決定するモードです。preferredAssetRepresentationModeのドキュメントに記載があるのですが、システムが追加のトランスコーディングを実行して、要求したアセットを互換性のある表現に変換する場合がある為、可能であれば、トランスコーディングを回避するために.currentを使用します。
  • configuration.selectionLimit
    • 何個選択することが出来るかを指定することが出来ます。デフォルト値は1で、今回は1つしか取得しない為、デフォルトを使用するので特に指定していません。

Coordinator

makeCoordinatorの箇所はUIImagePickerControllerの時と変わりがない為、割愛します。

Coordinatorクラスについて説明していきます。

class Coordinator: NSObject, PHPickerViewControllerDelegate {

    let parent: PhotoLibraryMoviePickerView

    init(_ parent: PhotoLibraryMoviePickerView) {
        self.parent = parent
    }

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

        parent.dismiss()

        guard let provider = results.first?.itemProvider else {
            return
        }

        let typeIdentifier = UTType.movie.identifier

        if provider.hasItemConformingToTypeIdentifier(typeIdentifier) {

            provider.loadFileRepresentation(forTypeIdentifier: typeIdentifier) { url, error in
                if let error = error {
                    print("error: \(error)")
                    return
                }
                if let url = url {
                    let fileName = "\(Int(Date().timeIntervalSince1970)).\(url.pathExtension)"
                    let newUrl = URL(fileURLWithPath: NSTemporaryDirectory() + fileName)
                    try? FileManager.default.copyItem(at: url, to: newUrl)
                    self.parent.movieUrl = newUrl
                }
            }
        }
    }
}

parent

PHPickerViewControllerのイベント伝達を受ける変数になります。

let parent: PhotoLibraryMoviePickerView

picker(_:, didFinishPicking:)

PHPickerViewControllerでメディアが選択された時に呼ばれるメソッドです。PHPickerViewControllerでは、didCancelのようなメソッドはなく、キャンセルが押された際もこのメソッドが呼ばれます。

なので、メソッドが呼ばれた場合にまずdismissを行い、メディアのURLが受け取れた場合は、値をバインディングするようにしています。

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

    parent.dismiss()

    guard let provider = results.first?.itemProvider else {
        return
    }

    let typeIdentifier = UTType.movie.identifier

    if provider.hasItemConformingToTypeIdentifier(typeIdentifier) {

        provider.loadFileRepresentation(forTypeIdentifier: typeIdentifier) { url, error in
            if let error = error {
                print("error: \(error)")
                return
            }
            if let url = url {
                let fileName = "\(Int(Date().timeIntervalSince1970)).\(url.pathExtension)"
                let newUrl = URL(fileURLWithPath: NSTemporaryDirectory() + fileName)
                try? FileManager.default.copyItem(at: url, to: newUrl)
                self.parent.movieUrl = newUrl
            }
        }
    }
}

今回は動画を取得したいのでtypeIdentifierには、オーディオとビデオを含むメディアを表すUTType.movie.identifierを代入しています。

provider.hasItemConformingToTypeIdentifier(typeIdentifier)でオーディオとビデオを含むメディアであるかを判定して、該当メディアである場合はURL取得処理を進めていきます。

provider.loadFileRepresentation(forTypeIdentifier:)で取得したファイルのデータのコピーを一時ファイルに書き込みます。一時ファイルは、完了ハンドラーが戻ったときにシステムが削除する為、ファイルのデータをTemporaryDirectoryにコピーしています。

let fileName = "\(Int(Date().timeIntervalSince1970)).\(url.pathExtension)"
let newUrl = URL(fileURLWithPath: NSTemporaryDirectory() + fileName)
try? FileManager.default.copyItem(at: url, to: newUrl)
self.parent.movieUrl = newUrl

これでPHPickerViewControllerを使用してフォトライブラリから動画を取得出来ました。

MoviePlayerView

動画の再生には、AVKitVideoPlayerを使用します。引数にAVPlayerを渡すことでコンテンツの再生を制御出来ます。またVideoPlayerには閉じるボタンが無いため、Viewの上部に閉じるボタンを追加しました。

import SwiftUI
import AVKit

struct MoviePlayerView: View {

    @Environment(\.dismiss) private var dismiss
    private let movieUrl: URL?

    init(with movieUrl: URL?) {
        self.movieUrl = movieUrl
    }

    var body: some View {
        VStack {
            HStack {
                Spacer()

                Button {
                    dismiss()
                } label: {
                    Text("Close")
                }

                Spacer()
                    .frame(width: 16)
            }
            VideoPlayer(player: AVPlayer(url: movieUrl!))
        }
    }
}

ContentView

各Pickerからの動画URLを保持するState変数movieUrlと、各PickerとMoviePlayerViewを表示するかどうかのフラグのBool値を用意しています。

各Picker用のButtonを押すと、それに紐づくPickerが表示されます。movieUrlnilでない場合は、再生ボタンが活性化し、押すと動画が再生されます。

import SwiftUI

struct ContentView: View {

    @State private var movieUrl: URL?
    @State private var showCameraMoviePickerView = false
    @State private var showPhotoLibraryMoviePickerView = false
    @State private var showMoviePlayerView = false

    private var canPlayVideo: Bool {
        movieUrl != nil
    }

    var body: some View {
        VStack(spacing: 32) {
            Spacer()

            Button {
                showCameraMoviePickerView = true
            } label: {
                Text("Camera Movie Picker")
            }

            Button {
                showPhotoLibraryMoviePickerView = true
            } label: {
                Text("Photo Library Movie Picker")
            }

            Button {
                showMoviePlayerView = true

                guard let url = movieUrl else {
                    return
                }
                print(url)
            } label: {
                Image(systemName: "play")
                    .resizable()
                    .frame(width: 50,
                           height:50)
                    .foregroundColor(canPlayVideo ? .accentColor : .gray)
            }
            .disabled(!canPlayVideo)

            Spacer()
        }
        .fullScreenCover(isPresented: $showCameraMoviePickerView) {
            CameraMoviePickerView(movieUrl: $movieUrl)
        }
        .fullScreenCover(isPresented: $showPhotoLibraryMoviePickerView) {
            PhotoLibraryMoviePickerView(movieUrl: $movieUrl)
        }
        .fullScreenCover(isPresented: $showMoviePlayerView) {
            MoviePlayerView(with: movieUrl)
        }
    }
}

これで完成です!

おわりに

これまではUIImagePickerControllerを使っていましたが、これからは積極的にPHPickerViewControllerを使っていきたいですね!

参考