0
0

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

Last updated at Posted at 2024-08-13

1. はじめに

こちらの記事の続きです

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

本文の内容は、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クラスのインスタンス化

前回の記事では、Coordinatorのクラスを実装しました。
今回の記事では、Coordinatorのインスタンスを作り、ビューコントローラーのデリゲートとして設定したいと思います。

本記事の「ビューコントローラー」とはUIImagePickerController()クラスのインスタンスを意味します。

ここで注意点が一つ。
クラスのインスタンスを作る時はvar 変数名 = クラス名()と書くのが定番ですが、Coordinatorのインスタンスを作る時には、この方式で書いてはいけません
なぜならdelegateプロパティは弱参照だから。以下のコード(失敗例)をご覧ください。

※失敗例(マネしないでください)
func makeUIViewController(context : Context) -> UIImagePickerController {
     let picker = UIImagePickerController()
+    picker.delegate = Coordinator()
     return picker
}

Coordinatorインスタンスは、ビューコントローラーpickerのプロパティ、delegateの参照先として生成されています。通常であれば、何の問題もないコードですが、delegateプロパティは弱参照のプロパティとなっています。
弱参照の参照先として生成されたインスタンスは、生成された直後に消失してしまう運命にあります。よって、picker.delegate = Coordinator()で生成されたインスタンスは、実際にはまったく使い物になりません。

それでは、どうしたらCoordinatorインスタンスをビューコントローラーのデリゲートとして設定することができるのか?
この問題を解決するのが、 makeCoordinator() メソッドです。

6. makeCoordinatorメソッドの性質

makeCoordinator() メソッドは、その名のとおり、Coordinatorのインスタンスを生成するメソッドです。
ただ、それだけでは、普通にインスタンス化するのとなんら変わらないですよね。
もちろん、makeCoordinator()はインスタンスを生成するだけのメソッドではありません。
makeCoordinator()ならではの性質があるからこそ、わざわざメソッドとして用意されているわけです。では、その性質とは何なのか?

公式ドキュメントで説明されているので引用します。

SwiftUI calls this method before calling the makeUIViewController(context:) method. The system provides your coordinator either directly or as part of a context structure when calling the other methods of your representable instance.

(訳)
SwiftUIはmakeUIViewController(context:)メソッドを呼び出す前にこのメソッドを呼び出します。システムはあなたのコーディネータを直接、もしくはあなたの表現可能なインスタンスの他のメソッドを呼び出すときにコンテキスト構造の一部として提供します。

引用元:公式ドキュメント

ここで注目すべきは以下の2箇所です。

  • SwiftUIはmakeUIViewController(context:)メソッドを呼び出す前にこのメソッドを呼び出します
  • (コーディネーターを)コンテキスト構造の一部として提供します

この記述から、makeCoordinator()は以下の3つの性質を持っていることがわかります。

(1) SwiftUIによって自動的に実行されるメソッドである
(2) makeUIViewController(context:)メソッドより先に実行される
(3) コンテクスト構造体のプロパティとしてコーディネーターを生成する

この3つの性質について、順番に見ていきましょう。

(1) SwiftUIによって自動的に実行されるメソッドである

これはmakeUIViewController(context:)メソッドにも見られた性質ですね。
SwiftUIによって自動的に実行されるので、メソッドを定義しさえすれば、呼び出すコードを書かなくても処理が実行されるようになるというわけです。

(2) makeUIViewController(context:)メソッドより先に実行される

これは実質、ImagePickerView構造体の中で一番最初に実行される関数はmakeCoordinator()メソッドだということです。ImagePickerView()のインスタンスが画面に表示されるとき、SwiftUIはまずmakeCoordinator()を実行し、Coordinatorのインスタンスを生成します。

(3) コンテクスト構造体のプロパティとしてコーディネーターを生成する

ここでいう「コンテクスト構造体」とは、構造体 UIViewControllerRepresentableContext のことです。また。「コーディネーター」とは Coordinatorクラスのインスタンスを意味します。

UIViewControllerRepresentableContext

システムの現在の状態に関する情報を格納し、UIViewControllerの作成と更新のため使用される構造体のこと。
ビューコントローラーを作成したり更新したりする際に、システムはUIViewControllerRepresentableContextのインスタンスを作成し、それを適切なメソッドに渡します。
UIViewControllerRepresentableContextはコーディネーターを格納する coordinator プロパティのほか、ビューのアニメーションを可能にするtransactionプロパティや、システムの現在の状態を表すプロパティenvironmentなどを所持しています。

makeCoordinator()メソッドは、Coordinatorインスタンス(=コーディネーター)を生成すると、UIViewControllerRepresentableContextcoordinatorプロパティとしてコーディネーターを設定します。
これにより、私たちは、makeUIViewController(context:)メソッドやupdateUIViewController(_:context:)メソッド内で、context.coordinatorを使用してコーディネーターにアクセスすることができるようになります。

7. makeCoordinatorメソッドの実装

makeCoordinator()メソッドについて概ね理解できたところで、実装に進んでみましょう。以下のとおり、コードを追加してください。

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
        return picker
    }
    
    func updateUIViewController(_ uiViewController : UIImagePickerController, context : Context){}
}

ここでは、さきほど触れたとおり、Coordinatorクラスのインスタンス(コーディネーター)の生成を行っております。Coordinator(parent : self)の箇所の self とは、構造体ImagePickerViewを指します。
生成されたコーディネーターはUIViewControllerRepresentabelContextcoorinatorプロパティに格納されます。

8. ビューコントローラーにデリゲートを設定する

ここでは、makeUIViewController(context:)で作成したビューコントローラーに、デリゲートを設定する方法について解説します。
その1で、makeUIViewController(context:)メソッドの中にpicker.delegateを参照するコードを書いたことを覚えているでしょうか?

ImagePickerView.swift(※その1より抜粋)
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){}
}

この長らく宙ぶらりんの状態になっていたpickerdelegateプロパティに対して、 コーディネーターを設定します。以下のとおり、コードを追記してください。

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

picker.delegate = context.coordinatorとなっているのは、コーディネーターのインスタンスが、UIViewRepresentabelContextcoorinatorプロパティに保存されているからです。

これで、ビューコントローラーのインスタンスとデリゲートの紐付けが完了しました。
コードとしてはこれで完成となります:sparkles:

カメラロールで画像を選ぶと、画面に表示されることをお確かめください:bow:

9. サンプルコードのライフサイクルについて

記事の最後に、より理解を深めるため、サンプルコードのライフサイクルがどうなっているのか順番に見ていきたいと思います。

1. インスタンスの生成

SwiftUIでImagepickerViewが表示されると、ImagePickerViewのインスタンスが生成されます。この時点で@Binding var image : UIImage?が初期化されます。

2. コーディネーターの生成

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

SwiftUIはmakeCoordinator()メソッドを呼び出し、コーディネーター(=Coordinatorクラスのインスタンス)を生成します。生成されたコーディネーターは構造体UIViewControllerRepresentableContextのプロパティとして格納されます。

3.ビューコントローラーの生成

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

SwiftUIはmakeUIViewController(context:)メソッドを呼び出し、ビューコントローラー(=UIImagePickerControllerクラスのインスタンス)を生成します。
ビューコントローラーには、デリゲートとしてコーディネーターが紐づけられます。
生成されたビューコントローラーは、SwiftUIのビューの中に組み込まれます。

4.ビューコントローラーの更新

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

ビューコントローラーが更新された場合には、updateUIViewController(_:context:)メソッドが呼び出され、UIImagePickerControllerを更新します。...とは書いていますが、今回はビューコントローラーの更新を行う必要がないため、サンプルコードを動かしても、updateUIViewController(_:context:)の処理が実行されることはありません。

5.ユーザーの操作結果を反映する

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

ユーザーが画像を選択すると、コーディネーターのimagePickerController()メソッドが呼び出されます。このメソッドによって、選択された画像が@Binding変数であるimageに格納され、ビューコントローラーが閉じられます。

10. おまけ Coordinatorクラスのインスタンス化の失敗例

5. Coordinatorクラスのインスタンス化では、Coordinatorクラスのインスタンスを作る時にvar 変数名 = クラス名()と書いてはいけないと述べていましたが、なぜvar 変数名 = クラス名()の書き方ではいけないのかをまとめましたので、おまけとして掲載します。

以下は失敗例としてのサンプルコードです。

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 makeUIViewController(context : Context) -> UIImagePickerController {
        let picker = UIImagePickerController()
        picker.delegate = Coordinator()
        return picker
    }

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

実際、XCodeで書いてみた画像がこちらです。
スクリーンショット 2024-08-09 1.02.25.png

警告が出ているので、コードに問題があるのは一目瞭然ですね。
なぜ警告が表示されるのかというと、以下の2つの問題があるからです。

  1. UIViewControllerRepresentableプロトコルに準拠していない
  2. UIImagePickerControllerのプロパティdelegateCoordinatorインスタンスを保持できない

それぞれ簡単に説明します。

問題1 UIViewControllerRepresentableプロトコルの条件を満たしていない

問題1は以下のコンパイルエラーの原因となっています。

Type 'ImagePickerView' does not conform to protocol 'UIViewControllerRepresentable'

(訳)
タイプ 'ImagePickerView' はプロトコル 'UIViewControllerRepresentable' に準拠していません。

UIViewControllerRepresentableに準拠するためには、以下の3つのメソッドを実装しなくてはなりません。

1. makeUIViewController(context:)
2. updateUIViewController(_:context:)
3. makeCoordinator()

公式ドキュメントのUIVireControllerRepresentableの項目には、makeCoordinator()メソッドについて以下のとおり記載されています。

func makeCoordinator() -> Self.Coordinator
Creates the custom instance that you use to communicate changes from your view controller to other parts of your SwiftUI interface.
Required Default implementation provided.

(訳)ビューコントローラーから SwiftUI インターフェイスの他の部分に変更を伝えるために使うカスタムインスタンスを作成します。
必須 デフォルトの実装が提供されます。

公式ドキュメントに書かれている内容は、つまりこういうことです。

  • makeCoordinator()メソッドは必ず実装しなければならない
  • makeCoordinator()メソッドはデフォルトで実装されるようになっているが、カスタムのCoordinatorが存在する場合には、オーバーライドしないとコンパイルエラーが起きてしまう

サンプルコードでカスタムのCoordinatorを作ってしまった以上、makeCoordinator()をオーバーライドしないとUViewControllerRepresentableプロトコルには準拠できないというわけですね。

問題点2. UIImagePickerControllerのプロパティdelegateCoordinatorインスタンスを保持できない

問題2は以下の警告メッセージが表示される原因となっています。

Instance will be immediately deallocated because property 'delegate' is 'weak'

(訳)プロパティ「delegate」が「weak」であるため、インスタンスは即座にデアロケートされる。

この警告メッセージは

  • UIImagePickerControllerdelegateプロパティが弱参照である
  • したがってdelegateプロパティに設定されたCoordinatorインスタンスは、作ったそばからメモリが解放されてしまうため、まったく使い物にならない

ということを意味します。では、弱参照とはどういうものでしょうか?

弱参照とは
クラスのインスタンスを参照しても、参照カウントをカウントアップしない参照方法のことです。

クラスのインスタンスへの参照には、強参照と弱参照の2種類があります。強参照は参照カウントを1つカウントアップし、弱参照はカウントアップしません。

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

Swiftでは、クラスのインスタンスを生成するとき、インスタンスの情報を記憶させておくために、必要範囲のメモリ領域を確保しますが、このインスタンスに割り当てられたメモリは、以下のように管理されます。

使用中のインスタンスのメモリが解放されてしまうことを防ぐために、プロパティ、変数、定数からそれぞれのクラスのインスタンスへの参照がいくつあるかをカウントしています。
このカウントが0になったとき、そのインスタンスはどこからも参照されてないとみなされ、メモリが解放されます。

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

インスタンスへの参照(参照カウント)が0になったとき、メモリが解放される。

これは裏を返すと、「参照カウントが0にならない限り、そのインスタンスのメモリは解放されない」ということです。そのため、お互いに相手を参照し合うような状態(循環参照)のインスタンスは永久に残り続けるということになります。

循環参照の状態になったインスタンスは、メモリを圧迫し、アプリのパフォーマンス低下を招きます。なので私たちは循環参照が起きるのを避けなくてはなりません。
弱参照は、以下のように参照カウントをカウントアップしないことで、この循環参照を回避することができます。

  • 通常、クラスのインスタンスを参照するときは参照カウントを+1するようになっている

  • 2つのインスタンスがお互いに参照しあうと、参照カウントが無限に増えていく循環参照の状態に陥ってしまう

  • クラスの参照方法を弱参照に変える

  • いくら参照がループしたとしても、弱参照は参照カウントをカウントアップしないので、参照カウントを0のままに留めておくことができるようになる

弱参照は、インスタンスを参照するプロパティにweakキーワードをつけることで、設定することができます。

もう一度、問題2の警告メッセージを見てみましょう。

Instance will be immediately deallocated because property 'delegate' is 'weak'

(訳)
プロパティ「delegate」が「weak」であるため、インスタンスは即座にデアロケートされる。

UIImagePickerController()の)delegateプロパティは弱参照であると、はっきり書かれてありますね。このことを踏まえて間違った書き方の例を見てみましょう。

※失敗例(マネしないでください)
let picker = UIImagePickerController()
picker.delegate = Coordinator()
return picker

間違った書き方では、delegateプロパティはpicker.delegate = Coordinator()というコードで設定されています。
delegateは弱参照のプロパティです。よって、delegateの値としてCoordinatorのインスタンスを生成したとしても、参照カウントは0のまま増えることはありません。
Coordinatorのインスタンスは参照カウントが0のため、どこからも参照されていないとみなされます。
その結果、メモリが解放され、Coordinatorインスタンスは生成された直後に消去されてしまうことになるのです。

以上の理由から、var 変数名 = クラス名()という書き方でCoordinatorクラスのインスタンスを生成してはいけないということになります。奥が深いですね。

11. おわりに

最後までお読みいただき、ありがとうございました:bow:
SwiftUI上でUIKitを使うコードは、コード自体は短い割に意外と内容が難しく、ChatGPT先生がいなかったら完全にお手上げな状態でした:sweat_smile: 公式ドキュメントを読み解く良い練習にもなったので、これからも公式を参照することを習慣にしたいと思います。
お疲れ様でした:tada:

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