1. はじめに
本記事では、UIKitの UIImagePickerController
クラスを使って、カメラロールから画像を選択する方法について解説します。
なぜUIImagePickerController
クラスを使うのかというと、そうしないとカメラロールから画像を選択する機能を実装することができないからです。
SwiftUIはまだ完全に成熟しきっていないため、このようにUIKitから部品を借りてこないと実装できない機能があるのです。
現在は、SwiftUIにもっと手軽にカメラロールを扱うことができる PhotosPicker
という構造体が用意されています。
「別にUIKitを使わなくてもいいよ」という方はここで読むのをやめて、以下の解説サイトを閲覧されることをおすすめします
全部で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. 完成イメージ
6. サンプルコード
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){}
}
7. なぜこの記事を書いたのか
今回、「UIImagePickerControllerを使って画像を選択しよう」というタイトルで3つの記事を書いたのは、書くことを通してUIKitに関する技術を理解したかったからです。
SwiftUIとUIKitは、お互いに異なるライフサイクルを持ったフレームワークです。
そのため、SwiftUIの中でUIKitのクラスを使用するには、両者が情報のやり取りができるよう、必要となるコードを抜かりなく記述する必要があります。
この過程が非常にややこしい…!
サンプルコードは、初見の私にはまったく意味がわからない代物でした。
これを1から説明できるようになれば、きっとUIKitを扱う技術が身につくはず!
そういう思いから執筆いたしました。
今ではPhotosPicker
があるので、UIKitなしでも「カメラロールから画像を選択する機能」を実装することは可能です。
8. サンプルコードの全体像について
サンプルコードの ContentView.swift と ImagePickerView.swift について、超ざっくりと解説します。
ContentView.swift
画面表示の見た目を担当する構造体
ImagePickerView.swift
SwiftUIに組み込むため、UIImagePickerController
クラスのインスタンスをUIViewControllerRepresentable
という専用のプロトコルでラッピングした構造体
上の画像はサンプルコードのImagePickerView.swiftを図で表したものです。
「SwiftUIの世界」の中に、UIViewControllerRepresentable
プロトコルでラップされた「UIKitの世界」がありますね。文字通りSwiftUIに「組み込まれて」います。
UIViewControllerRepresentable
プロトコルの囲みの中には、画像選択のインターフェースを提供するUIImagePickerController
クラスのインスタンスが配置されています。
隣には Coordinator
という名前の人影がありますね。これはコーディネーターといって、「coordinator = 交渉人」の名前のとおり、UIKit側の交渉人として振る舞い、SwiftUIとUIKitが情報をやり取りできるよう、間に入って調整してくれます。
9. UIViewControllerRepresentable
UIImagePickerController
はUIKitのクラスなので、SwiftUIの世界では生きられません。
淡水魚を海水に放つようなものです。
UIImagePickerController
クラスをSwiftUIで扱うためには、SwiftUIの中にUIKitが生きられる環境を作ってあげる必要があり、その役目を担うのが UIViewControllerRepresentable
プロトコルです。
UIViewControllerRepresentable
は、UIKitのビューコントローラーをSwiftUIの一部として使用できるようにするプロトコルです。
ちなみにUIViewControllerRepresentable
の名称には「represent = 代表する・表現する」という単語が含まれていますが、これはUIViewControllerRepresentable
プロトコルが、UIKitのビューコントローラーをSwiftUIのビューとして表現(represent)するからです。
ビューコントローラーとは
画面に表示されるビューを管理したり操作したりする役割を持つクラスのこと。
たとえばユーザーインターフェースを構築したり、コンポーネントの表示・非表示を切り替えたり、ビューの表示にアニメーションをかけたりする処理を行います。
それでは、さっそくImagePickerView
という名前で構造体を宣言して、UIViewControllerRepresentable
プロトコルに準拠させてみましょう。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
プロトコル。ビューコントローラそのものとなっています。
上記のコードを記述すると、XCode上に以下の警告メッセージが表示されます。
Type 'ImagePickerView' does not conform to protocol 'UIViewControllerRepresentable'
Add stubs for conformance(訳)
タイプ'ImagePickerView'がプロトコル'UIViewControllerRepresentable'に適合していない。スタブを追加する。
すると、今度はtypealias UIViewControllerType = type
という文が出現し、以下の警告が表示されます。
Cannot find type '<#type#>' in scope
(訳)
スコープ内で型 '<#type#>' が見つからない。
なぜ警告が表示されるのでしょうか。
それはUIViewControllerRepresentable
プロトコルに準拠するため、「何のビューコントローラーをSwiftUIのビューとして表現するのか」をはっきりさせないといけないからです。
今回はUIImagePickerController
のインスタンスをSwiftUIのビューとして表現するので、以下のようにコードを追記します。
import SwiftUI
import UIKit
struct ImagePickerView : UIViewControllerRepresentable {
+ typealias UIViewControllerType = UIImagePickerController
@Binding var image : UIImage?
}
さて、警告は消えたでしょうか?
消えていませんね...。もう1回Fix
ボタンを押してみましょう。
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つの処理をコードに書いてみましょう。
-
makeUIViewController(context:)
メソッドの実装 - ビューコントローラの作成
-
updateUIViewController(_:context:)
メソッドの実装
以下のとおり、コードを記述してください。
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です。
これで、カメラロールから画像を選択するビューコントローラーを作ることができました!さっそく、動作をシミュレーターで確かめてみましょう。
カメラロールの選択画面は表示されましたが、選択した画像が画面に表示されていません。これはいけませんね。しかしなぜ、カメラロールの選択画像が画面に表示されなかったのでしょうか?
それは「UIKitのビューコントローラー内での変更は、自動的にはSwiftUIの他の部分へ伝達されないようになっている」からです。
SwiftUIとUIKitは、お互いに異なるライフサイクルを持っているので、直接情報をやり取りすることができません。そのためUIKitのビューコントローラー内で起こった変更は、そのままではSwiftUIの他の部分に反映されません。UIKitの代わりにSwiftUIと情報のやり取りを行なってくれるオブジェクト、デリゲートの存在が必要になります。
デリゲート
deligate = (権限などを)委譲・委任する
あるオブジェクトが実行できないような処理を、別のオブジェクトに実行してもらう仕組みのことを、プログラミングの分野ではデリゲートと読んでいます。
今回の場合、デリゲートは「ユーザーが画像や動画を選択したときや、選択をキャンセルしたときに通知を受け取り、指定された処理を実行するオブジェクト」と解釈していただけばOKです。
ビューコントローラー内での変更をSwiftUIの他の部分に反映させるには、デリゲートを作成し、ビューコントローラーのインスタンスと紐づける必要があります。UIImagePickerController
クラスにはそのためのプロパティ delegate
が用意されています。
デリゲートをどうやって作成するのかについては、次回以降の記事で説明するとして、ここではプロパティのdelgete
を参照するコードを書き、デリゲートと紐づけするための準備をしておきましょう。以下のとおり、コードを追記してください。
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
について解説していきます。それでは