【iOS/SwiftUI】写真アプリから画像を選択して表示させる

SwiftUIに戻る

目次

概要

自分で作ったアプリで写真アプリの画像を使うということは、ちゃんと画像を変数に格納する必要がある。
今回はその実装部分を記載する。

SwiftUIで写真アプリから画像を複数取得する処理はなかなか記載されていなかったのでメモ。

ソースコード

import SwiftUI

@main
struct TestApp: App {
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    
    var body: some Scene {
        WindowGroup {
            PhotoView()
        }
    }
}
import SwiftUI

struct PhotoView: View {
    @ObservedObject private var viewModel = PhotoViewModel()
    
    @State var selectedImages: [UIImage] = []
    
    var body: some View {
        VStack {
            HStack {
                Spacer()
                Button {
                    viewModel.isShownPhotoList = true
                } label: {
                    Text("フォトライブラリ表示")
                }
                Spacer()
                Button {
                    selectedImages.removeAll()
                } label: {
                    Text("クリア")
                }
                Spacer()
            }
            ScrollView {
                ForEach(selectedImages, id: \.self) { image in
                    Image(uiImage: image)
                        .resizable()
                        .aspectRatio(contentMode: .fit)
                        .frame(width: UIScreen.main.bounds.width)
                }
            }
        }
        .sheet(isPresented: $viewModel.isShownPhotoList) {
            PhotoPickerView(selectedImages: $selectedImages)
        }
    }
}
import SwiftUI
import PhotosUI

struct PhotoPickerView: UIViewControllerRepresentable {
    var configuration: PHPickerConfiguration
    let completion: (([PHPickerResult]) -> Void)?
    
    @Binding var selectedImages: [UIImage]
    
    init(selectedImages: Binding<[UIImage]>, completion: (([PHPickerResult]) -> Void)? = nil) {
        configuration = PHPickerConfiguration(photoLibrary: .shared())
        configuration.filter = .images
        configuration.preferredAssetRepresentationMode = .current
        configuration.selectionLimit = 5
        
        self.completion = completion
        
        self._selectedImages = selectedImages
    }
    
    func makeUIViewController(context: Context) -> PHPickerViewController {
        let controller = PHPickerViewController(configuration: configuration)
        controller.delegate = context.coordinator
        controller.isModalInPresentation = true
        return controller
    }
    
    func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {
        
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
}

class Coordinator: NSObject {
    private var parent: PhotoPickerView
    init(_ parent: PhotoPickerView) {
        self.parent = parent
    }
}

extension Coordinator: PHPickerViewControllerDelegate {
    func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
        results.forEach { result in
            let itemProvider = result.itemProvider
            if itemProvider.canLoadObject(ofClass: UIImage.self) {
                itemProvider.loadObject(ofClass: UIImage.self) { [weak self] image, error in
                    guard let self,
                          let image = image as? UIImage else {
                        return
                    }
                    self.parent.selectedImages.append(image)
                }
            }
        }
        picker.dismiss(animated: true)
    }
}

デモ動画

詳細

PhotoView.swift

初期設定と選択可能な上限数の設定

まずは以下の部分で初期設定を行う。
一度にselectionLimitで選択できる数を5にしている。

configuration = PHPickerConfiguration(photoLibrary: .shared())
configuration.filter = .images
configuration.preferredAssetRepresentationMode = .current
configuration.selectionLimit = 5

選択した画像を変数に格納し、画面に反映させる

そして、以下の箇所で選んだ画像をselectedImagesに格納し、@Bindingで定義することでその格納した結果がPhotoViewのselectedImagesに反映されるようにしている。

@Binding var selectedImages: [UIImage]
    
init(selectedImages: Binding<[UIImage]>, completion: (([PHPickerResult]) -> Void)? = nil) {
    // 中略
    
    self._selectedImages = selectedImages
}
extension Coordinator: PHPickerViewControllerDelegate {
    func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
    // 中略
                    guard let self,
                          let image = image as? UIImage else {
                        return
                    }
                    self.parent.selectedImages.append(image)
    // 中略y
    }
}

@Bindingに関しては別の記事で記載するが、とあるクラスで変更した内容を別のクラスの変数に反映させてViewを更新したい時は@Bindingで設定する。
しかし、それだけでは初期化に関するエラーが発生する。そのため、@Bindingの初期化は変数名の前に「_」をつけることで@Binding<T>として初期化することができる。

ちなみに呼び出す側はPhotoPickerView.swiftの項目で記載する。

選択した画像のデータをUIImageのオブジェクトに変換する

選択した画像のデータはPHPickerResult型になっている。それをUIImage型に変換するにはPHPickerResultのitemProviderを元に考える。

extension Coordinator: PHPickerViewControllerDelegate {
    func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
        results.forEach { result in
            let itemProvider = result.itemProvider
            if itemProvider.canLoadObject(ofClass: UIImage.self) {
                itemProvider.loadObject(ofClass: UIImage.self) { [weak self] image, error in
                    guard let self,
                          let image = image as? UIImage else {
                        return
                    }
                    self.parent.selectedImages.append(image)
                }
            }
        }
        picker.dismiss(animated: true)
    }
}

以下のようにitemProviderがUIImageに変換できるかどうかを確認する。

itemProvider.canLoadObject(ofClass: UIImage.self)

そして、得られたUIImage型のオブジェクトを以下の処理を行う。
要はselectedImagesにUIImage型ののオブジェクトを追加する。
そして、追加されてselectedImagesが更新されたら、それが@Bindingの効果でPhotoPickerViewに反映される。

{ [weak self] image, error in
    guard let self,
          let image = image as? UIImage else {
        return
    }
    self.parent.selectedImages.append(image)
}

PhotoPickerView.swift

まず、画像の一覧を表示する箇所は以下になる。
@State型の変数を@Bindingに格納する。こうすることで、PhotoPickerViewで変更された内容を画面に反映させることができる。

.sheet(isPresented: $viewModel.isShownPhotoList) {
    PhotoPickerView(selectedImages: $selectedImages)
}

そして、上記でPhotoPickerViewで変更された内容を表示するために、以下のようにselectedImagesで繰り返し文を記載する。

ScrollView {
    ForEach(selectedImages, id: \.self) { image in
        Image(uiImage: image)
            .resizable()
            .aspectRatio(contentMode: .fit)
            .frame(width: UIScreen.main.bounds.width)
    }
}

参考ページ

SwiftUIに戻る