0
1

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

Last updated at Posted at 2024-08-13

1. はじめに

本記事では、UIKitの UIImagePickerController クラスを使って、カメラロールから画像を選択する方法について解説します。

なぜUIImagePickerControllerクラスを使うのかというと、そうしないとカメラロールから画像を選択する機能を実装することができないからです。
SwiftUIはまだ完全に成熟しきっていないため、このようにUIKitから部品を借りてこないと実装できない機能があるのです。

現在は、SwiftUIにもっと手軽にカメラロールを扱うことができる PhotosPicker という構造体が用意されています。

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

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

全部で3つの記事があり、本記事はその1番目の記事となります。

  • UIKitをSwiftUIで使ってみたい

という方には、お役に立てる内容となっているかと思います。
よろしくお願いします。

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

UIImagePickerController
以下の操作を行うためのインターフェースを提供するクラスのこと。

  • 写真を撮影する
  • ビデオを録画する
  • カメラロールから画像や動画を選択する

2. 開発環境

  • OS : Sonoma 14.5
  • Xcode : Version 15.4
  • Simulator : Version 15.4

3. 全体の構成

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

4. 目次

1. はじめに
2. 開発環境
3. 全体の構成
4. 目次
5. 完成イメージ
6. サンプルコード
7. なぜこの記事を書いたのか
8. サンプルコードの全体像について
9. UIViewControllerRepresentable
10. ビューコントローラーの作成
11. 次の記事へ

5. 完成イメージ

ImagePickerGIF.gif

6. サンプルコード

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

7. なぜこの記事を書いたのか

今回、「UIImagePickerControllerを使って画像を選択しよう」というタイトルで3つの記事を書いたのは、書くことを通してUIKitに関する技術を理解したかったからです。

SwiftUIとUIKitは、お互いに異なるライフサイクルを持ったフレームワークです。
そのため、SwiftUIの中でUIKitのクラスを使用するには、両者が情報のやり取りができるよう、必要となるコードを抜かりなく記述する必要があります。

この過程が非常にややこしい…!

サンプルコードは、初見の私にはまったく意味がわからない代物でした。
これを1から説明できるようになれば、きっとUIKitを扱う技術が身につくはず!
そういう思いから執筆いたしました。

今ではPhotosPickerがあるので、UIKitなしでも「カメラロールから画像を選択する機能」を実装することは可能です。


8. サンプルコードの全体像について

サンプルコードContentView.swiftImagePickerView.swift について、超ざっくりと解説します。

ContentView.swift
画面表示の見た目を担当する構造体

ImagePickerView.swift
SwiftUIに組み込むため、UIImagePickerControllerクラスのインスタンスをUIViewControllerRepresentableという専用のプロトコルでラッピングした構造体

サンプルコードの構成図
サンプルコードの構成図

上の画像はサンプルコードImagePickerView.swiftを図で表したものです。

「SwiftUIの世界」の中に、UIViewControllerRepresentableプロトコルでラップされた「UIKitの世界」がありますね。文字通りSwiftUIに「組み込まれて」います。

UIViewControllerRepresentableプロトコルの囲みの中には、画像選択のインターフェースを提供するUIImagePickerControllerクラスのインスタンスが配置されています。

隣には Coordinatorという名前の人影がありますね。これはコーディネーターといって、「coordinator = 交渉人」の名前のとおり、UIKit側の交渉人として振る舞い、SwiftUIとUIKitが情報をやり取りできるよう、間に入って調整してくれます。

9. UIViewControllerRepresentable

サンプルコードの構成図(UIViewControllerRepresentableにフォーカス)

UIImagePickerControllerはUIKitのクラスなので、SwiftUIの世界では生きられません。
淡水魚を海水に放つようなものです。
fish

UIImagePickerControllerクラスをSwiftUIで扱うためには、SwiftUIの中にUIKitが生きられる環境を作ってあげる必要があり、その役目を担うのが UIViewControllerRepresentable プロトコルです。 

UIViewControllerRepresentableは、UIKitのビューコントローラーをSwiftUIの一部として使用できるようにするプロトコルです。

ちなみにUIViewControllerRepresentableの名称には「represent = 代表する・表現する」という単語が含まれていますが、これはUIViewControllerRepresentableプロトコルが、UIKitのビューコントローラーをSwiftUIのビューとして表現(represent)するからです。

ビューコントローラーとは
画面に表示されるビューを管理したり操作したりする役割を持つクラスのこと。
たとえばユーザーインターフェースを構築したり、コンポーネントの表示・非表示を切り替えたり、ビューの表示にアニメーションをかけたりする処理を行います。

参考:SwiftでViewControllerを使う

それでは、さっそくImagePickerViewという名前で構造体を宣言して、UIViewControllerRepresentableプロトコルに準拠させてみましょう。ImagePickerView.swiftに以下のコードを記述してください。

ImagePickerView.swift
import SwiftUI
import UIKit

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

@Binding var image : UIImage?

@Bindingアノテーションを付けて変数imageを定義することで、imageが変更された際に、ContentView.swift のプロパティimageも同期して更新されるようにしています。

struct ImagePickerView : UIViewControllerRepresentable

構造体ImagePickerViewが準拠しているのはViewではなくUIViewControllerRepresentableプロトコル。ビューコントローラそのものとなっています。

参考:Wrapping a UIViewController in a SwiftUI view

上記のコードを記述すると、XCode上に以下の警告メッセージが表示されます。

Type 'ImagePickerView' does not conform to protocol 'UIViewControllerRepresentable'
Add stubs for conformance

(訳)
タイプ'ImagePickerView'がプロトコル'UIViewControllerRepresentable'に適合していない。スタブを追加する。

スクリーンショット 2024-08-01 0.30.17.png
隣にあるFixボタンを押してみましょう。

スクリーンショット 2024-08-01 0.36.36.png
すると、今度はtypealias UIViewControllerType = typeという文が出現し、以下の警告が表示されます。

Cannot find type '<#type#>' in scope

(訳)
スコープ内で型 '<#type#>' が見つからない。

なぜ警告が表示されるのでしょうか。
それはUIViewControllerRepresentableプロトコルに準拠するため、「何のビューコントローラーをSwiftUIのビューとして表現するのか」をはっきりさせないといけないからです。

今回はUIImagePickerControllerのインスタンスをSwiftUIのビューとして表現するので、以下のようにコードを追記します。

ImagePickerView.swift
import SwiftUI
import UIKit

struct ImagePickerView : UIViewControllerRepresentable {
+   typealias UIViewControllerType = UIImagePickerController

    @Binding var image : UIImage?
}

さて、警告は消えたでしょうか?
スクリーンショット 2024-08-01 1.10.14 2.png
消えていませんね...。もう1回Fixボタンを押してみましょう。

スクリーンショット 2024-08-01 1.16.06.png
すると、新たに2つのメソッドが構造体の中に出現しました。

  • makeUIViewController(context:)
  • updateUIViewController(_:context:)

UIViewControllerRepresentableプロトコルに準拠するためには、さらにこの2つのメソッドを実装しなくてはなりません。

makeUIViewController(context:)

makeUIViewController(context:) は、UIViewControllerRepresentableプロトコルのメソッドの一つで、UIKitのビューコントローラーを作って返してくれます
このメソッドを定義しないとUIViewControllerRepresentableプロトコルに準拠できず、コンパイルエラーになってしまいます。

UIViewControllerRepresentableは、SwiftUIでビューコントローラーを使用可能にするためのプロトコルです。makeUIViewController(context:)メソッドを実装しなければ、

「は?なんでビューコントローラーを作るメソッドも無いのに、UIViewControllerRepresentableを名乗ってんの?」

ということになってしまいます。だから、実装は必須なのでしょうね。

またmakeUIViewController(context:)メソッドは自分を呼び出すコードを書く必要がないという謎の属性をもっています。
ふつう関数を作ったら、どこかでその関数を呼び出すコードを書きますよね?
しかしmakeUIViewController(context:)メソッドは、contextという引数まで持っているくせに、宣言するだけで勝手に実行されてしまうのです。
では、誰がmakeUIViewController(context:)メソッドを実行しているのか?

SwiftUIです。

SwiftUIはビューコントローラーを作る際に一度だけmakeUIView(context:)メソッドを実行します。SwiftUIが自動でやってくれるので、makeUIViewController(context:)メソッドを呼び出す必要はありません。引数のcontextもSwiftUIが埋めてくれます。便利ですね。

参考:Using coordinators to manage SwiftUI view controllers


updateUIViewController(_:context:)

updateUIViewController(_:context:)メソッドは、ビューコントローラーの状態を更新するメソッドです。1つ目の引数の_で対象となるビューコントローラーを指定し、2つ目の引数のcontextでSwiftUI側の最新の状態の情報をメソッドへと渡します。
指定したビューコントローラーに影響を与えるような変更が起こった場合に限り、自動的にこのメソッドが呼ばれます。

今回は以下のように定義すればOKです。

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

「えっ?{}の中、空っぽじゃん...。」
そうです。この updateUIViewController(_:context:)メソッドは、{}の中に何も処理を書かなくても実装することができます。なぜでしょうか?

ビューコントローラーはmakeUIViewController(context:)メソッドで生成されます。
しかし、生成されたビューコントローラーがその後いっさい変更されないのなら、更新処理を記述する必要はありません。だから{}の中が空っぽでも問題ないというわけです。


10. ビューコントローラーの作成

makeUIViewController(context:)メソッドとupdateUIViewController(_:context:)メソッドの概要が理解できたところで、次の3つの処理をコードに書いてみましょう。

  1. makeUIViewController(context:)メソッドの実装
  2. ビューコントローラの作成
  3. updateUIViewController(_:context:)メソッドの実装

以下のとおり、コードを記述してください。

ImagePickerView.swift
import SwiftUI
import UIKit

struct ImagePickerView : UIViewControllerRepresentable {

    @Binding var image : UIImage?
    
    typealias UIViewControllerType = UIImagePickerController
    
+    func makeUIViewController(context: Context) -> UIImagePickerController {
+        let picker = UIImagePickerController()
+        return picker
+    }
    
+    func updateUIViewController(_ uiViewController:UIImagePickerController,
+      context: Context) {}
}

makeUIViewController(context:)メソッドを実装するためには、戻り値として何らかのビューコントローラーを返さなければなりません。

ここではUIImagePickerController()クラスのインスタンスを、ビューコントローラーとして返すことで、「1. makeUIViewController(context:)メソッドの実装」と「2. ビューコントローラの作成」の2つの処理が実行されるようになっています。  

「3.updateUIViewController(_:context:)の実装」については、先ほどのセクションで記載したコードをそのまま転記すればOKです。

これで、カメラロールから画像を選択するビューコントローラーを作ることができました!さっそく、動作をシミュレーターで確かめてみましょう。

UIImagePickerController_step1

カメラロールの選択画面は表示されましたが、選択した画像が画面に表示されていません。これはいけませんね。しかしなぜ、カメラロールの選択画像が画面に表示されなかったのでしょうか?
それは「UIKitのビューコントローラー内での変更は、自動的にはSwiftUIの他の部分へ伝達されないようになっている」からです。

SwiftUIとUIKitは、お互いに異なるライフサイクルを持っているので、直接情報をやり取りすることができません。そのためUIKitのビューコントローラー内で起こった変更は、そのままではSwiftUIの他の部分に反映されません。UIKitの代わりにSwiftUIと情報のやり取りを行なってくれるオブジェクト、デリゲートの存在が必要になります。

デリゲート
deligate = (権限などを)委譲・委任する

あるオブジェクトが実行できないような処理を、別のオブジェクトに実行してもらう仕組みのことを、プログラミングの分野ではデリゲートと読んでいます。

今回の場合、デリゲートは「ユーザーが画像や動画を選択したときや、選択をキャンセルしたときに通知を受け取り、指定された処理を実行するオブジェクト」と解釈していただけばOKです。

ビューコントローラー内での変更をSwiftUIの他の部分に反映させるには、デリゲートを作成し、ビューコントローラーのインスタンスと紐づける必要があります。UIImagePickerControllerクラスにはそのためのプロパティ delegate が用意されています。

デリゲートをどうやって作成するのかについては、次回以降の記事で説明するとして、ここではプロパティのdelgeteを参照するコードを書き、デリゲートと紐づけするための準備をしておきましょう。以下のとおり、コードを追記してください。

ImagePickerView.swift
import SwiftUI
import UIKit

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

    typealias UIViewControllerType = UIImagePickerController

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

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

ここで追記したpicker.delegateになんらかの値を代入することで、UIImagePickerControllerクラスのインスタンスとデリゲートを紐づけることができます。

11. 次の記事へ

お疲れ様でした。今回はここまでとなります。
次の記事では、デリゲートを生成する元となるクラス、Coordinatore について解説していきます。それでは:wave:

0
1
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
1