[SwiftUI] カメラorライブラリから写真を取り込む

# はじめに
SwiftUIで作った画面に写真をカメラorライブラリから一枚撮影or選択する機能を実装したく調べました。

# 環境
Xcode 15.4
iOS15 ~

# 内容

### カメラの表示
カメラを動かすだけであれば`UIImagePickerController`を使う方法が一番簡単そうでした。
SwiftUIで扱うため、`UIViewControllerRepresentable`を使います。

https://developer.apple.com/documentation/uikit/uiimagepickercontroller

```swift
import SwiftUI

struct CameraCaptureView: UIViewControllerRepresentable {
    @Binding var image: UIImage?
    @Environment(\.presentationMode) private var presentationMode

    func makeUIViewController(context: Context) -> UIImagePickerController {
        let picker = UIImagePickerController()
        picker.sourceType = .camera
        picker.delegate = context.coordinator
        picker.allowsEditing = true
        return picker
    }

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

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

    class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
        let parent: CameraCaptureView

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

        func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
            if let uiImage = info[.originalImage] as? UIImage {
                parent.image = uiImage
            }

            parent.presentationMode.wrappedValue.dismiss()
        }

        func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
            parent.presentationMode.wrappedValue.dismiss()
        }
    }
}
```

ここでの`Coordinator`は`UIImagePickerControllerDelegate`と`UINavigationControllerDelegate`に準拠する必要があリます。

```swift:UIImagePickerController
weak open var delegate: (any UIImagePickerControllerDelegate & UINavigationControllerDelegate)?
```

### ライブラリの表示

ライブラリを表示するため、`PHPickerViewController`を使います。（iOS16以上であれば[`PhotosPicker`](https://developer.apple.com/documentation/photokit/photospicker)が使えます）
カメラ同様`UIImagePickerController`を使っても表示することはできますが、こちらはすでに[非推奨](https://developer.apple.com/documentation/uikit/uiimagepickercontroller/sourcetype/photolibrary)となっております。
こちらもSwiftUIで扱えるように`UIViewControllerRepresentable`を使います。

https://developer.apple.com/documentation/photokit/phpickerviewcontroller

ちなみに`UIImagePickerController`から`PHPickerViewController`にすることで以下のような改良されているようです。

- 遅延画像読み込みとリカバリーUI
- RAW画像やパノラマ画像などの大型で複雑なアセットの確実な処理
- UIImagePickerControllerでは利用できないユーザー選択可能なアセット
- Live Photosのみを表示するようにピッカーを構成可能
- ライブラリアクセスなしでPHLivePhotoオブジェクトを利用可能
- 無効な入力に対するより厳密な検証
- (オプションメニューはPHPickerViewControllerのみ表示される)

```swift
import SwiftUI
import PhotosUI

struct PhotoLibraryPickerView: UIViewControllerRepresentable {
    @Binding var image: UIImage?
    @Environment(\.presentationMode) private var presentationMode

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

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

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

    class Coordinator: NSObject, PHPickerViewControllerDelegate {
        let parent: PhotoLibraryPickerView

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

        func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
            parent.presentationMode.wrappedValue.dismiss()

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

            if provider.canLoadObject(ofClass: UIImage.self) {
                provider.loadObject(ofClass: UIImage.self) { image, _ in
                    DispatchQueue.main.async {
                        self.parent.image = image as? UIImage
                    }
                }
            }
        }
    }
}
```

### カメラ・ライブラリから写真を読み込む

カメラとライブラリのオプションを表示して、写真を取り込む画面の最小実装をしてみました。
カメラの表示はフルスクリーンにするように[ドキュメントに記載](https://developer.apple.com/documentation/uikit/uiimagepickercontroller)がありましたので、`fullScreenCover`を使います。

```swift
.fullScreenCover(isPresented: $showCamera) {
    CameraCaptureView(image: $image)
        .ignoresSafeArea()
}
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631585/394d8e6a-3c9d-f29d-00f2-3f9e39bc8d9f.png" width="200"><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631585/b881d1fc-c473-2434-d7ee-3e82c10c214d.png" width="200"><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631585/3ab53d75-b3cb-75d5-cca0-446baefefe29.png" width="200">


```swift
import SwiftUI

struct PhotoPickerView: View {
    @State var image: UIImage?
    @State private var showImagePickerDialog = false
    @State private var showCamera: Bool = false
    @State private var showLibrary: Bool = false

    var body: some View {
        VStack(spacing: 20) {
            Text("写真を追加")

            if let image {
                Image(uiImage: image)
                    .resizable()
                    .aspectRatio(contentMode: .fit)

                Button("削除する") {
                    self.image = nil
                }
            } else {
                Button("選択する") {
                    showImagePickerDialog = true
                }
            }
        }
        .fullScreenCover(isPresented: $showCamera) {
            CameraCaptureView(image: $image)
                .ignoresSafeArea()
        }
        .sheet(isPresented: $showLibrary, content: {
            PhotoLibraryPickerView(image: $image)
                .ignoresSafeArea()
        })
        .confirmationDialog(
            "",
            isPresented: $showImagePickerDialog,
            titleVisibility: .hidden
        ) {
            Button {
                showCamera = true
            } label: {
                Text("カメラで撮る")
            }
            Button {
                showLibrary = true
            } label: {
                Text("アルバムから選ぶ")
            }
            Button("キャンセル", role: .cancel) {
                showImagePickerDialog = false
            }
        }
    }
}
```

# おわりに

iOSのサポートバージョン次第では不要な実装も含まれているかと思います。
iOS17以上であれば`PhotosPicker`でインラインのフォトピッカーが簡単に実装できそうなので、この辺りも調べていきたいと思います。

https://developer.apple.com/documentation/photokit/implementing_an_inline_photos_picker

# 参考
https://developer.apple.com/documentation/uikit/uiimagepickercontroller

https://developer.apple.com/documentation/photokit/phpickerviewcontroller

https://zenn.dev/yorifuji/articles/swiftui-imagepicker