0
0

【Swift】UIImagePickerControllerを使って画像を選択しよう(その2)

Last updated at Posted at 2024-08-13

1. はじめに

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

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

本文の内容は、ChatGPTに質問して得た回答を下敷きに、公式ドキュメント等で裏を取りながら執筆しました。
また、英文の和訳はDeepL翻訳を用いて訳したものを記載しています。

SwiftUIには、もっと手軽にカメラロールを扱うことができる PhotosPicker という構造体があります。

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

【SwiftUI】iOS16+のPhotosPicker|フルSwiftUIでのフォトピッカー実装方法

2. 全体の構成

タイトル 概要
UIImagePickerControllerを使って画像を選択しよう
(その1)
サンプルコードの全体像
UIViewControllerRepresentable
UIImagePickerControllerを使って画像を選択しよう
(その2)
Coordinatorの作成
UIImagePickerControllerを使って画像を選択しよう
(その3)
デリゲートの設定
その他

3. 目次


4. サンプルコード

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()
    }
}

本記事では、ContentView.swift に関する説明を割愛させていただきます。
ご了承ください。

↓ 以下、 ImagePickerView.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){}
}

5. Coordinatorクラスのプロトコル

スクリーンショット 2024-08-13 17.55.25.png
前回の記事では、ビューコントローラー内での変更をSwiftUIに反映させるには、デリゲートを作成し、ビューコントローラーのインスタンスと紐づける必要があると説明しました。

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

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

Coordinatorは、UIKitのビューコントローラーであるUIImagePickerControllerとやり取りするために、

  • NSObjxectクラス
  • UINavigationControllerDelegateプロトコル
  • UIImagePickerControllerDelegateプロトコル

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

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って何者?少しだけ詳しく調べてみた


UINavigationControllerDelegateプロトコル

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

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

【参考】公式ドキュメント


UIImagePickerControllerDelegate

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

  • カメラロールで選択した画像の内容で、SwiftUIの構造体のプロパティを書き換える

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

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

  • UINavigationControllerDelegate
  • UIImagePickerControllerDelegate

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


6. Coordinatorクラスの初期化

このセクションでは、Coordinatorクラスを初期化する処理について説明します。
以下のコードを追記してください。

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が役目を果たすためには、まず自分がどのインスタンスの交渉人なのかをはっきりさせなければなりません。それを行なっているのが

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

の箇所になります。ここではじめて Coordinatorは引数parentで指定したインスタンスの交渉人であるということが確定し、構造体ImagePickerViewを参照できるようになります。

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

7. imagePickerControllerメソッド

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

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:)は、実際に呼び出すコードを書く必要がないメソッドです。コードを書かなくても自動的に実行されるのは、その1の記事にて解説したmakeUIViewController(context:)メソッドと同じですね。

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

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

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


imagePickerControllerメソッドの引数

imagePickerControllerの引数について、説明します。
公式ドキュメントより、型の定義を引用します。

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

引用元:公式ドキュメント imagePickerController(_:didFinishPickingMediaWithInfo:)

内部引数と外部引数について

引数名は関数の呼び出し時に使用する外部引数名と、関数内で使用される内部引数名の2つを持つことができます。外部引数名と内部引数名を分けるには、外部引数名 内部引数名: 型という形式で引数を定義します。

引用元:
[増補改訂第3版]Swift実践入門 直感的な文法と安全性を兼ね備えた言語
P.130,131

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

picker

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

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

info

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

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.

(訳)
ユーザーが選択したメディアタイプ。


引用元 公式ドキュメント UIImagePickerController.InfoKey

imagePickerControllerメソッドの実装

メソッドの処理の実装について説明します。以下のコードをご覧ください。

※一部抜粋
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へとダウンキャストしようとしています。ダウンキャストに成功した場合はselectedImageUIImageが格納され、失敗した場合にはnilが格納されます。

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はこのようにして、UIKitSwiftUIの交渉人の役目を果たしているんですね。

picker.dismiss(animated : true)

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


8. imagePickerControllerDidCancelメソッド

以下の通り、コードを追記してください。

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){}
}
※一部抜粋
func imagePickerControllerDidCancel(_ picker : UIImagePickerController){
    picker.dismiss(animated : true)
}

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

9. 次の記事へ

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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0