1. はじめに
こちらの記事の続きです
本記事ではUIKitのクラス UIImagePickerController
を使って、カメラロールから画像を選択する方法について解説します。
全部で3つの記事があり、本記事はその2番目の記事となります。
本文の内容は、ChatGPTに質問して得た回答を下敷きに、公式ドキュメント等で裏を取りながら執筆しました。
また、英文の和訳はDeepL翻訳を用いて訳したものを記載しています。
SwiftUIには、もっと手軽にカメラロールを扱うことができる PhotosPicker
という構造体があります。
「別にUIKitを使わなくてもいいよ」という方はここで読むのをやめて、以下の解説サイトを閲覧されることをおすすめします
2. 全体の構成
タイトル | 概要 |
---|---|
UIImagePickerControllerを使って画像を選択しよう (その1) |
サンプルコードの全体像 UIViewControllerRepresentable |
UIImagePickerControllerを使って画像を選択しよう (その2) |
Coordinatorの作成 |
UIImagePickerControllerを使って画像を選択しよう (その3) |
デリゲートの設定 その他 |
3. 目次
- 1. はじめに
- 2. 全体の構成
- 3. 目次
- 4. サンプルコード
- 5. Coordinatorクラスのプロトコル
- 6. Coordinatorクラスの初期化
- 7. imagePickerControllerメソッド
- 8. imagePickerControllerDidCancelメソッド
- 9. 次の記事へ
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クラスのプロトコル
前回の記事では、ビューコントローラー内での変更をSwiftUIに反映させるには、デリゲートを作成し、ビューコントローラーのインスタンスと紐づける必要があると説明しました。
Coordinator
は、そのデリゲートを生成する元となるクラスです。
デリゲート
ここでは、ユーザーがビューコントローラー内で画像や動画を選択したときや、選択をキャンセルしたときに通知を受け取り、指定された処理を実行するオブジェクトのことをデリゲートと呼びます。
Coordinator
は、UIKitのビューコントローラーであるUIImagePickerController
とやり取りするために、
- NSObjxectクラス
- UINavigationControllerDelegateプロトコル
- UIImagePickerControllerDelegateプロトコル
のすべてに準拠する必要があります。
以下のとおり、コードを記述してください。
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
クラスを初期化する処理について説明します。
以下のコードを追記してください。
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
を参照できるようになります。
そして、Coordinator
はImagePickerView
インスタンスを参照し、ImagePickerView
の@Binding
プロパティを更新することで、UIKitの情報をSwiftUIに渡すことができるのです。このことについては、また後で触れたいと思います。
7. imagePickerControllerメソッド
このセクションでは、Coordinator
クラスに imagePickerController(_:didFinishPickingMediaWithInfo:)
メソッドを実装する方法について説明します。以下のコードを追記してください。
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
クラスのインスタンスは、画像が選択されると次に行うべき処理を委任するために、Coordinator
のimagePickerController(_: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
へとダウンキャストしようとしています。ダウンキャストに成功した場合はselectedImage
にUIImage
が格納され、失敗した場合には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
はこのようにして、UIKit
とSwiftUI
の交渉人の役目を果たしているんですね。
picker.dismiss(animated : true)
picker.dismiss(animated : true)
では、表示中のカメラロールを閉じる処理を実行しています。
dismiss
とはUIKitのメソッドで、モーダルに表示されているビューコントローラを閉じるはたらきをします。
この場合、表示されているビューコントローラはUIImagePickerController
クラスのインスタンスだから、カメラロールを閉じることになったのですね。
ちなみに、外部引数animated
では、モーダルを閉じる動作をアニメーションにするか否かを設定することができます。アニメーションが無いと味気ないので、ここではtrue
と設定しました。
8. imagePickerControllerDidCancelメソッド
以下の通り、コードを追記してください。
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
のインスタンスを作り、ビューコントローラーのデリゲートとして設定したいと思います。それでは