1. はじめに
こちらの記事の続きです
本記事ではUIKitの UIImagePickerController
クラスを使って、カメラロールから画像を選択する方法について解説します。
全部で3つの記事があり、本記事はその3番目の記事となります。
本文の内容は、ChatGPTに質問して得た回答を下敷きに、公式ドキュメント等で裏を取りながら執筆しました。
また、英文の和訳はDeepL翻訳を用いて訳したものを記載しています。
SwiftUIには、もっと手軽にカメラロールを扱うことができる PhotosPicker
という構造体があります。
「別にUIKitを使わなくてもいいよ」という方はここで読むのをやめて、以下の解説サイトを閲覧されることをおすすめします
2. 全体の構成
タイトル | 概要 |
---|---|
UIImagePickerControllerを使って画像を選択しよう (その1) |
サンプルコードの全体像 UIViewControllerRepresentable |
UIImagePickerControllerを使って画像を選択しよう (その2) |
Coordinatorの作成 |
UIImagePickerControllerを使って画像を選択しよう (その3) |
デリゲートの設定 その他 |
3. 目次
- 1. はじめに
- 2. 全体の構成
- 3. 目次
- 4. サンプルコード
- 5. Coordinatorクラスのインスタンス化
- 6. makeCoordinatorメソッドの性質
- 7. makeCoordinatorメソッドの実装
- 8. ビューコントローラーにデリゲートを設定する
- 9. サンプルコードのライフサイクルについて
- 10. おまけ Coordinatorクラスのインスタンス化の失敗例
- 11. おわりに
4. サンプルコード
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 について説明します
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
インスタンス(=コーディネーター)を生成すると、UIViewControllerRepresentableContext
のcoordinator
プロパティとしてコーディネーターを設定します。
これにより、私たちは、makeUIViewController(context:)
メソッドやupdateUIViewController(_:context:)
メソッド内で、context.coordinator
を使用してコーディネーターにアクセスすることができるようになります。
7. makeCoordinatorメソッドの実装
makeCoordinator()
メソッドについて概ね理解できたところで、実装に進んでみましょう。以下のとおり、コードを追加してください。
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
を指します。
生成されたコーディネーターはUIViewControllerRepresentabelContext
のcoorinator
プロパティに格納されます。
8. ビューコントローラーにデリゲートを設定する
ここでは、makeUIViewController(context:)
で作成したビューコントローラーに、デリゲートを設定する方法について解説します。
その1で、makeUIViewController(context:)
メソッドの中にpicker.delegate
を参照するコードを書いたことを覚えているでしょうか?
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
プロパティに対して、 コーディネーターを設定します。以下のとおり、コードを追記してください。
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
となっているのは、コーディネーターのインスタンスが、UIViewRepresentabelContext
のcoorinator
プロパティに保存されているからです。
これで、ビューコントローラーのインスタンスとデリゲートの紐付けが完了しました。
コードとしてはこれで完成となります
カメラロールで画像を選ぶと、画面に表示されることをお確かめください
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 変数名 = クラス名()
の書き方ではいけないのかをまとめましたので、おまけとして掲載します。
以下は失敗例としてのサンプルコードです。
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){}
}
警告が出ているので、コードに問題があるのは一目瞭然ですね。
なぜ警告が表示されるのかというと、以下の2つの問題があるからです。
-
UIViewControllerRepresentable
プロトコルに準拠していない -
UIImagePickerController
のプロパティdelegate
はCoordinator
インスタンスを保持できない
それぞれ簡単に説明します。
問題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
のプロパティdelegate
はCoordinator
インスタンスを保持できない
問題2は以下の警告メッセージが表示される原因となっています。
Instance will be immediately deallocated because property 'delegate' is 'weak'
(訳)プロパティ「delegate」が「weak」であるため、インスタンスは即座にデアロケートされる。
この警告メッセージは
-
UIImagePickerController
のdelegate
プロパティが弱参照である - したがって
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. おわりに
最後までお読みいただき、ありがとうございました
SwiftUI上でUIKitを使うコードは、コード自体は短い割に意外と内容が難しく、ChatGPT先生がいなかったら完全にお手上げな状態でした 公式ドキュメントを読み解く良い練習にもなったので、これからも公式を参照することを習慣にしたいと思います。
お疲れ様でした