【Swift】UIImagePickerControllerを使って画像を選択しよう（その２）

# 1. はじめに

こちらの記事の続きです:arrow_down:

https://qiita.com/Hiroki01100001/items/e98c76ad0406263e198b

本記事ではUIKitのクラス **`UIImagePickerController`** を使って、カメラロールから画像を選択する方法について解説します。
全部で3つの記事があり、本記事はその**2番目**の記事となります。

:::note warn
本文の内容は、ChatGPTに質問して得た回答を下敷きに、公式ドキュメント等で裏を取りながら執筆しました。
また、英文の和訳はDeepL翻訳を用いて訳したものを記載しています。
:::
:::note info
SwiftUIには、もっと手軽にカメラロールを扱うことができる **`PhotosPicker`** という構造体があります。

「別にUIKitを使わなくてもいいよ」という方はここで読むのをやめて、以下の解説サイトを閲覧されることをおすすめします:thumbsup:

 [【SwiftUI】iOS16+のPhotosPicker｜フルSwiftUIでのフォトピッカー実装方法](https://blog.code-candy.com/swiftui_photospicker/)
:::

# 2. 全体の構成
|タイトル  |概要  |
|---|---|
|[UIImagePickerControllerを使って画像を選択しよう<br>（その１）](https://qiita.com/Hiroki01100001/items/e98c76ad0406263e198b)|サンプルコードの全体像<br>UIViewControllerRepresentable|
|UIImagePickerControllerを使って画像を選択しよう<br>（その２）|Coordinatorの作成|
|[UIImagePickerControllerを使って画像を選択しよう<br>（その３）](https://qiita.com/Hiroki01100001/items/e049379c54c3e07096cc)|デリゲートの設定<br>その他|

# 3. 目次
- [1. はじめに](#1-はじめに)
- [2. 全体の構成](#2-全体の構成)
- [3. 目次](#3-目次)
- [4. サンプルコード](#4-サンプルコード)
- [5. Coordinatorクラスのプロトコル](#5-coordinatorクラスのプロトコル)
- [6. Coordinatorクラスの初期化](#6-coordinatorクラスの初期化)
- [7. imagePickerControllerメソッド](#7-imagepickercontrollerメソッド)
- [8. imagePickerControllerDidCancelメソッド](#8-imagepickercontrollerdidcancelメソッド)
- [9. 次の記事へ](#9-次の記事へ)
<br>

# 4. サンプルコード
```swift:ContentView.swift
import SwiftUI

struct ContentView: View {
    @State private var image : UIImage?
    @State var isShowing = false

    var body : some View {
        VStack {
            Button(action : {
                isShowing = true
            }){
                Text("Select Image")
            }
            .sheet(isPresented : $isShowing){
                ImagePickerView(image : $image)
            }
            if let image = image {
                Image(uiImage : image)
                    .resizable()
                    .frame(width : 350, height: 350)
            }   
        }
    }
}

struct ContentView_Previews : PreviewProvider {
    static var previews : some View {
        ContentView()
    }
}
```
:::note warn
本記事では、**ContentView.swift** に関する説明を割愛させていただきます。
ご了承ください。
:::


↓ 以下、 **ImagePickerView.swift** について説明します
``` swift:ImagePickerView.swift
import SwiftUI
import UIKit

struct ImagePickerView : UIViewControllerRepresentable {
    @Binding var image : UIImage?

    typealias UIViewControllerType = UIImagePickerController

    class Coordinator : NSObject, 
                        UINavigationControllerDelegate,
                        UIImagePickerControllerDelegate {
        var parent : ImagePickerView

        init(parent : ImagePickerView){
            self.parent = parent
        }

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

        func imagePickerControllerDidCancel(_ picker : UIImagePickerController){
            picker.dismiss(animated : true)
        }
    }

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

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

    func updateUIViewController(_ uiViewController : UIImagePickerController, context : Context){}
}
```
<br>

# 5. Coordinatorクラスのプロトコル
![スクリーンショット 2024-08-13 17.55.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/589104/127e26ae-fe65-2cbd-2d0f-bd662aeb55ed.png)
前回の記事では、ビューコントローラー内での変更をSwiftUIに反映させるには、デリゲートを作成し、ビューコントローラーのインスタンスと紐づける必要があると説明しました。

**`Coordinator`** は、その**デリゲートを生成する元となるクラス**です。

:::note info
**デリゲート**
ここでは、ユーザーがビューコントローラー内で画像や動画を選択したときや、選択をキャンセルしたときに通知を受け取り、指定された処理を実行するオブジェクトのことを**デリゲート**と呼びます。

:::

`Coordinator`は、**UIKit**のビューコントローラーである`UIImagePickerController`とやり取りするために、
- **NSObjxect**クラス
- **UINavigationControllerDelegate**プロトコル
- **UIImagePickerControllerDelegate**プロトコル

のすべてに準拠する必要があります。
以下のとおり、コードを記述してください。

```diff_Swift: ImagePickerView.swift
import SwiftUI
import UIKit

struct ImagePickerView : UIViewControllerRepresentable {
    @Binding var image : UIImage?

    typealias UIViewControllerType = UIImagePickerController
    
+    class Coordinator : NSObject,
+                        UINavigationControllerDelegate,
+                        UIImagePickerControllerDelegate {
+    }

    func makeUIViewController(context : Context) -> UIImagePickerController {
        let picker = UIImagePickerController()
        picker.delegate
        return picker
    }

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

上記のとおりにコードを書いたら、
>`Type 'ImagePickerView' does not conform to protocol 'UIViewControllerRepresentable'`
>
>(訳)
>タイプ'ImagePickerView'はプロトコル'UIViewControllerRepresentable'`に準拠していません。

と、警告が表示されるかと思いますが、これについては後ほど説明します。まずは`Coordinator`が準拠している3つのプロトコルについて解説します。

## NSObjectクラス
**`NSObject`** クラスはプログラミング言語、Objective-Cのルートクラスのことです。UIKitは、Objective-Cでも動作するように作られているため、ほとんどのUIKitが`NSObject`クラスを継承しています。したがって、`Coordinator`がUIKitと情報のやり取りを行うためには`NSObject`プロトコルに準拠する必要があります。

 【参考】　[【Swift】NSObjectって何者？少しだけ詳しく調べてみた](https://qiita.com/Ryuto_Yuz/items/ce7f2091fea17a9408e8)

https://developer.apple.com/documentation/objectivec/nsobject
 
<br>

## UINavigationControllerDelegateプロトコル
**`UINavigationControllerDelefate`** は、`UINavigationController`の動作をカスタマイズするために使用されるプロトコルです。準拠することで、`UINavigationController`クラスの処理に任意の動作を追加できるようになります。

:::note info
**UINavigationController**
ビューコントローラを配列の形で管理するクラスのこと。
配列の要素を追加したり削除したりすることで、画面に表示するビューコントローラを操作することができます。

【参考】[公式ドキュメント](https://developer.apple.com/documentation/uikit/uinavigationcontroller)
:::

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

<br>

## UIImagePickerControllerDelegate
**`UIImagePickerControllerDelegate`** は、`UIImagePickerController`クラスのインスタンスが画像や動画の選択を完了したときに、通知を受け取るためのプロトコルです。
準拠することで、ユーザーが画像を選択したり、選択をキャンセルした後、任意の処理を実行できるようになります。たとえば、今回のケースを例に挙げると、
- カメラロールで選択した画像の内容で、SwiftUIの構造体のプロパティを書き換える

といった処理ができるようになります。

ちなみに`Coordinator`クラスのインスタンスは、後ほど`UIImagePickerController`のインスタンスの **`delegate`** プロパティとして位置付けられますが、この`delegate`プロパティは
- `UINavigationControllerDelegate` 
- `UIImagePickerControllerDelegate` 

の両方のプロトコルに準拠する必要があります。
そのため、`Coordinator`クラスの実装では **`UINavigationControllerDelegate`**　と **`UIImagePickerControllerDelegate`** の2つのプロトコルに準拠しなければならないのです。

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

<br>

# 6. Coordinatorクラスの初期化
このセクションでは、`Coordinator`クラスを初期化する処理について説明します。
以下のコードを追記してください。
```diff_Swift: ImagePickerView.swift
import SwiftUI
import UIKit

struct ImagePickerView : UIViewControllerRepresentable {
    @Binding var image : UIImage?

    typealias UIViewControllerType = UIImagePickerController
    
    class Coordinator : NSObject,
                        UINavigationControllerDelegate,
                        UIImagePickerControllerDelegate {      
+        var parent : ImagePickerView
+        init(parent : ImagePickerView){
+            self.parent = parent
+        }
    }

    func makeUIViewController(context : Context) -> UIImagePickerController {
        let picker = UIImagePickerController()
        picker.delegate
        return picker
    }

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

初期化処理では、**`Coordinator`がどのインスタンンスに関連づけられるのか**を設定します。

`Coordinator`は、日本語だと**調整役・交渉人**という意味ですが、もし`Coordinator`がどのインスタンスにも関連づけられないとしたら、それは「自分はどこの会社にも勤めていませんが、あなたと交渉したいです！」と言っているようなものです。
そんな人とは、怖くて交渉なんかできませんよね。

`Coordinator`が役目を果たすためには、まず**自分がどのインスタンスの交渉人なのか**をはっきりさせなければなりません。それを行なっているのが
```swift:
   var parent : ImagePickerView
    init(parent : ImagePickerView){
        self.parent = parent
    }
```
の箇所になります。ここではじめて **`Coordinator`は引数`parent`で指定したインスタンスの交渉人である**ということが確定し、構造体`ImagePickerView`を参照できるようになります。

そして、`Coordinator`は`ImagePickerView`インスタンスを参照し、`ImagePickerView`の`@Binding`プロパティを更新することで、UIKitの情報をSwiftUIに渡すことができるのです。このことについては、また後で触れたいと思います。
<br>

# 7. imagePickerControllerメソッド
このセクションでは、`Coordinator`クラスに **`imagePickerController(_:didFinishPickingMediaWithInfo:)`** メソッドを実装する方法について説明します。以下のコードを追記してください。

```diff_swift:ImagePickerView.swift
import SwiftUI
import UIKit

struct ImagePickerView : UIViewControllerRepresentable {
    @Binding var image : UIImage?

    typealias UIViewControllerType = UIImagePickerController

    class Coordinator : NSObject,
                        UINavigationControllerDelegate,
                        UIImagePickerControllerDelegate {
        var parent : ImagePickerView
            init(parent : ImagePickerView){
            self.parent = parent
        }

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

    func makeUIViewController(context : Context) -> UIImagePickerController {
        let picker = UIImagePickerController()
        picker.delegate
        return picker
    }

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

`imagePickerController(_:didFinishPickingMediaWithInfo:)`メソッドは公式ドキュメントに以下のように定義されています。
>Tells the delegate that the user picked a still image or movie.
>
>(訳)
>ユーザが静止画または動画を選択したことをデリゲートに伝えます。

ただ、定義の内容がいまいちピンと来ないので、私はこのメソッドを**ビューコントローラーの操作後に実行する処理を設定する**関数と捉えております（認識が間違っているかもしれませんが...）。

`imagePickerController(_:didFinishPickingMediaWithInfo:)`は、**実際に呼び出すコードを書く必要がない**メソッドです。コードを書かなくても自動的に実行されるのは、[その１](https://qiita.com/Hiroki01100001/items/e98c76ad0406263e198b)の記事にて解説した`makeUIViewController(context:)`メソッドと同じですね。

では、何が`imagePickerController(_:didFinishPickingMediaWithInfo:)`を呼び出すのか？それはビューコントローラーである　**`UIImagePickerController`** クラスのインスタンスです。

`UIImagePickerController`クラスのインスタンスは、画像が選択されると次に行うべき処理を委任するために、`Coordinator`の`imagePickerController(_:didFinishPickingMediaWithInfo:)`メソッドを呼び出すのです。

つまり、`imagePickerController(_:didFinishPickingMediaWithInfo:)`メソッドは、ユーザーがメディアの選択を完了したときに**自動的に呼び出され、次に行うべき処理を委任される**メソッドと言えます。

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

<br>

## imagePickerControllerメソッドの引数
`imagePickerController`の引数について、説明します。
公式ドキュメントより、型の定義を引用します。
> ```swift
> optional func imagePickerController(
>   _ picker: UIImagePickerController,
>   didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]
>)
> ```
> 引用元：[公式ドキュメント　imagePickerController(_:didFinishPickingMediaWithInfo:)](https://developer.apple.com/documentation/uikit/uiimagepickercontrollerdelegate/1619126-imagepickercontroller)

:::note info
**内部引数と外部引数について**
>引数名は関数の呼び出し時に使用する外部引数名と、関数内で使用される内部引数名の２つを持つことができます。外部引数名と内部引数名を分けるには、**外部引数名 内部引数名: 型**という形式で引数を定義します。
>
>引用元：
>[増補改訂第３版]Swift実践入門　直感的な文法と安全性を兼ね備えた言語
>P.130,131

`imagePickerController`の場合は、第１引数の **`_`** は外部引数名、 **`picker`** は内部引数名、 **`UIImagePickerController`**　は型ということになります。
※ちなみに引数名が`_`となっているのは、外部引数を省略することを示しています。
:::

### picker
>The controller object managing the image picker interface.
>(訳)
>イメージピッカーインタフェースを管理するコントローラオブジェクト。

`UIImagePickerController`クラスが型として指定されているとおり、内部引数 **`picker`** には`UIImagePickerController`クラスのインスタンスが渡されることになります。
<br>

### info
内部引数 **`info`** には、画像が選ばれた場合にはオリジナル画像と編集後の画像を含む**辞書**が、動画が選ばれた場合は動画のファイルURLを含む辞書が格納されます。
辞書とは`[キー　：　値]`のように「キー」と「値」がペアになったコレクションのことを指し、辞書のキーは　**`UIImagePickerController.InfoKey`** に格納されます。
<br>

### UIImagePickerController.InfoKey
内部引数`info`の型である`[UIImagePickerController.InfoKey : Any]`は、**選択したメディアの情報を取得するために使用するキー**のことです。以下にキーの値の例を抜粋します。

**`originalImage`**
> static let originalImage: UIImagePickerController.InfoKey
The original, uncropped image selected by the user.
>
>(訳)
>ユーザーが選択した、トリミングされていないオリジナルの画像。

**`editedImage`**
>An image edited by the user.
>
>(訳）
>ユーザーが編集した画像。

**`mediaType`**
>The media type selected by the user.
>
>(訳)
>ユーザーが選択したメディアタイプ。
<br>

引用元　[公式ドキュメント　UIImagePickerController.InfoKey](https://developer.apple.com/documentation/uikit/uiimagepickercontroller/infokey)

## imagePickerControllerメソッドの実装
メソッドの処理の実装について説明します。以下のコードをご覧ください。
```swift:※一部抜粋
func imagePickerController(_ picker : UIImagePickerController,
    didFinishPickingMediaWithInfo info : [UIImagePickerController.InfoKey : Any]){
    if let selectedImage = info[.originalImage] as? UIImage {
        parent.image = selectedImage
    }
    picker.dismiss(animated : true)
}
```

#### `if let selectImage = info[.originalImage] as? UIImage` について
`info[.originalImage]`では、`originalImage`をキーにして、辞書`info`の中からユーザーが選択した元画像を取り出そうとしています。

`as? UIImage`では、`as`演算子を使って、元画像のデータを`UIImage`へとダウンキャストしようとしています。ダウンキャストに成功した場合は`selectedImage`に`UIImage`が格納され、失敗した場合には`nil`が格納されます。
<br>

#### `parent.image = selectedImage` について
`parent.image = selectedImage`では、選択画像の`UIImage`データをもって、`parent.image`を書き換えています。
ここで`Coordinator`を初期化するときの話を思い出してください。
`parent`とは、**`Coordinator`が所属するインスタンスを決める引数**で、その型は **`UIImagePickerView`** となっていました。
そのため、`parent.image`は、`ImagePickerView`インスタンスの`image`プロパティ、`@Binding var image : UIImage?`のことを指します。これを`selectedImage`で書き換えることはつまり、`@Binding var image : UIImage?`を選択画像の`UIImage`データに書き換えることを意味します。
言い換えるなら`parent.image = selectedImage`では、**UIKitの変更情報をもって、SwiftUIのプロパティを更新している**のです。
`Coordinator`はこのようにして、`UIKit`と`SwiftUI`の交渉人の役目を果たしているんですね。
<br>

#### `picker.dismiss(animated : true)`
`picker.dismiss(animated : true)`では、表示中のカメラロールを閉じる処理を実行しています。
`dismiss`とはUIKitのメソッドで、モーダルに表示されているビューコントローラを閉じるはたらきをします。
この場合、表示されているビューコントローラは`UIImagePickerController`クラスのインスタンスだから、カメラロールを閉じることになったのですね。
ちなみに、外部引数`animated`では、モーダルを閉じる動作をアニメーションにするか否かを設定することができます。アニメーションが無いと味気ないので、ここでは`true`と設定しました。


<br>

# 8. imagePickerControllerDidCancelメソッド
以下の通り、コードを追記してください。
```diff_Swift: ImagePickerView.swift
import SwiftUI
import UIKit

struct ImagePickerView : UIViewControllerRepresentable {
    @Binding var image : UIImage?

    typealias UIViewControllerType = UIImagePickerController
    
    class Coordinator : NSObject,
                        UINavigationControllerDelegate,
                        UIImagePickerControllerDelegate {
        func imagePickerController(_ picker : UIImagePickerController,
            didFinishPickingMediaWithInfo info : [UIImagePickerController.InfoKey : Any]){
            if let selectedImage = info[.originalImage] as? UIImage {
                parent.image = selectedImage
            }
            picker.dismiss(animated : true)
        }

+        func imagePickerControllerDidCancel(_ picker : UIImagePickerController){
+            picker.dismiss(animated : true)
+        }
    }

    func makeUIViewController(context : Context) -> UIImagePickerController {
        let picker = UIImagePickerController()
        picker.delegate
        return picker
    }

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

```swift:※一部抜粋
func imagePickerControllerDidCancel(_ picker : UIImagePickerController){
    picker.dismiss(animated : true)
}
```

**`imagePickerControllerDidCancel(_:)`** メソッドについて、少しだけ解説します。
これは、ユーザーが画像選択をキャンセルしたことを伝達するメソッドです。
内部で行なっている処理は、ただ一つ。カメラロールを閉じることだけです。
`picker.dismiss(animated : true)`で閉じる動作を実行しているのですが、この内容については`imagePickerController(_:didFinishPickingMediaWithInfo:)`メソッドで述べたので、ここでは割愛させていただきます。
<br>

# 9. 次の記事へ
お疲れ様でした。今回はここまでとなります。
次の記事では、`Coordinator`のインスタンスを作り、ビューコントローラーのデリゲートとして設定したいと思います。それでは:wave: