UIImagePickerControllerをUIViewControllerから表示させる方法について記述している記事は多くありましたが、UIViewからそれを行う記事があまり見当たらなかったので、今回記事を書いてみることにしました。
UIViewからUIImagePickerを表示させること自体がレアなケースだと思いますが、参考になれば幸いです。
動作イメージ
なぜそれが必要になったのか
今回とあるアプリを作成している中で、UIViewControllerに UIStackViewとUICollectionView が置いてあり、UICollectionViewのそれぞれのCellをタップすると、別途作成しているUIViewがinsertされるといった実装を行っておりました。なぜこのような実装について語ると長くなりますが、簡単に言えば異なるUIViewを画面遷移せずにViewControllerで表示したかった為です。
そんな中、あるUIViewでユーザーから写真を入力してもらう必要性がありました。
そこで、自分なりの正解に辿り着いたので、以下に記述します。
必要なもの
・UIImagePickerController
・UIPopoverPresentationControllerDelegate
・UIApplicationのExtension(最前面のViewControllerを判定する)
・UIImagePickerControllerDelegate(タップされた写真を判定)
・UINavigationControllerDelegate(タップされた写真を通知)
必要な処理
この実装にはUIViewで画面遷移を行う方法と、UIViewからViewControllerにタップイベントを通知して、VC側で画面遷移を行う方法があります。
UIView側で画面遷移を行うのは、少し愚直的であり、設計的にも良く無いと思いますが、今回は両方ご紹介します。
1. UIViewで画面遷移を行う方法
まずは、UIimagePickerControllerのインスタンスを作成します。
この中で、souceTypeや遷移の方法を設定します。
5行目については、.any
でも問題ありません。
private let imagePickerView: UIImagePickerController = {
let picker = UIImagePickerController()
picker.sourceType = .photoLibrary
picker.modalPresentationStyle = .popover
picker.popoverPresentationController?.permittedArrowDirections = .up
return picker
}()
次に、それを呼ぶためのトリガーとなるもの(今回はUIButton)を設定し、タップアクション等によって、以下のアクションを行うようにします。
@objc private func buttonDidTap() {
imagePickerView.popoverPresentationController?.sourceView = self
imagePickerView.popoverPresentationController?.delegate = self
UIApplication.topViewController()?.present(imagePickerView, animated: true)
}
※ここで記述しているUIApplication.topViewController()
では最前面にあるViewController
を判定し、それを用いてUIViewから.present()
でUIImagePickerController()
を表示させています。
実装については下のコードをご覧いただけますと幸いです。
その他、
imagePickerView.delegate = self
やfunc imagePickerController(...)
については、それらの動作で取得した写真をimageViewに当てはめてる処理になりますので、説明は省きます。
UIView側で画面遷移する際のコードの全体像
動作確認済みです。StoryBoardを使用しない実装を行い、制約についてはSnapKitを使用しています。
レイアウトについてはあくまでサンプルアプリである為、その点ご容赦ください。
import UIKit
import SnapKit
class HogeUIView: UIView {
private let titleLabel: UILabel = {
let label = UILabel()
label.text = "I'm UIView."
label.font = .systemFont(ofSize: 15)
label.textAlignment = .center
return label
}()
private let addButton: UIButton = {
let button = UIButton()
button.backgroundColor = .blue
button.setTitle("写真を追加します", for: .normal)
button.addTarget(.none, action: #selector(buttonDidTap), for: .touchUpInside)
return button
}()
private let imageView: UIImageView = {
let image = UIImageView()
image.backgroundColor = .gray
image.contentMode = .scaleToFill
return image
}()
private lazy var entireStack: UIStackView = {
let stack = UIStackView(arrangedSubviews: [titleLabel, imageView, addButton])
stack.axis = .vertical
stack.distribution = .fill
stack.spacing = 10
stack.setCustomSpacing(30, after: imageView)
stack.layer.borderWidth = 1
stack.isLayoutMarginsRelativeArrangement = true
stack.layoutMargins = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20)
return stack
}()
private let imagePickerView: UIImagePickerController = {
let picker = UIImagePickerController()
picker.sourceType = .photoLibrary
picker.modalPresentationStyle = .popover
picker.popoverPresentationController?.permittedArrowDirections = .up
return picker
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupViews() {
addSubview(entireStack)
imagePickerView.delegate = self
entireStack.snp.makeConstraints { make in
make.width.equalTo(300)
make.center.equalToSuperview()
}
addButton.snp.makeConstraints { make in
make.height.equalTo(40)
}
imageView.snp.makeConstraints { make in
make.height.equalTo(260)
}
}
}
extension HogeUIView: UIImagePickerControllerDelegate, UINavigationControllerDelegate, UIPopoverPresentationControllerDelegate {
@objc private func buttonDidTap() {
imagePickerView.popoverPresentationController?.sourceView = self
imagePickerView.popoverPresentationController?.delegate = self
UIApplication.topViewController()?.present(imagePickerView, animated: true)
}
func imagePickerController(_: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
guard let selectedImage = info[UIImagePickerController.InfoKey.originalImage] as? UIImage else { return }
imageView.image = selectedImage
UIApplication.topViewController()?.dismiss(animated: true)
}
}
import UIKit
import SnapKit
class ViewController: UIViewController {
private let label: UILabel = {
let label = UILabel()
label.text = "I'm ViewController."
label.font = .systemFont(ofSize: 20)
label.textAlignment = .center
return label
}()
private lazy var entireStackView: UIStackView = {
let stack = UIStackView(arrangedSubviews: [label])
stack.axis = .vertical
stack.distribution = .fill
stack.alignment = .fill
stack.spacing = 10
stack.isLayoutMarginsRelativeArrangement = true
stack.layoutMargins = UIEdgeInsets(top: 100, left: 20, bottom: 50, right: 20)
return stack
}()
override func viewDidLoad() {
super.viewDidLoad()
setupViews()
view.backgroundColor = .white
}
private func setupViews() {
view.addSubview(entireStackView)
let view = HogeUIView()
entireStackView.insertArrangedSubview(view, at: 1)
entireStackView.snp.makeConstraints { make in
make.width.equalToSuperview()
make.height.equalTo(800)
make.center.equalToSuperview()
}
}
}
extension UIApplication {
static func topViewController() -> UIViewController? {
let scenes = UIApplication.shared.connectedScenes
let windowScene = scenes.first as? UIWindowScene
let window = windowScene?.windows.first
guard var top = window?.rootViewController else {
return nil
}
while let next = top.presentedViewController {
top = next
}
return top
}
}
2. UIViewのタップイベントをVCに通知して、VCで画面遷移を行う方法
こちらについては、どのようにタップイベントをVCに通知すべきかについてのみ解説します。
その中で行うUIImagePicker等の表示・遷移のコードが重複するためです。
今回はRxSwiftを使って通知処理を書いています。
class HogeView: UIView {
var buttonDidTap: Observable<Void> {
addButton.rx.tap.asObservable()
}
private let addButton: UIButton = {
let button = UIButton()
button.layer.cornerRadius = 20
button.backgroundColor = .systemTeal
button.setTitle("写真を選択", for: .normal)
return button
}()
class ViewController: UIViewController {
//これをViewDidLoad内で呼んでください。
func bind() {
customView.buttonDidTap
//customViewはUIViewをインスタンス化したものです
.subscribe(onNext: { [weak self] _ in
self.onTapedButton()//タップされた際の処理
})
.disposed(by: disposeBag)
}
あとは、UIView側で画面遷移を行うコード内のdelegete
の記述やタップアクションをこちらに移行することで、動作します。
さらに、こちらの場合はUIApplication
のExtensionは不要となり、
- UIApplication.topViewController()?.dismiss(animated: true)
+ present(imagePickerController, animated: true, completion: nil)
このように記述することができます。
※UIView側でsetupView()で記述しているimagePickerView.delegate = self
もVC側に書くことを忘れないように注意ください。
最後に
今回の実装で、お役に立てれば幸いです。
また、おそらくこれ以外の実装方法も多くあると思いますので参考までにお願いします。
参考文献
Apple Developer - UIImagePicker
Apple Developer - UIPopoverPresentationController
StackOverFlow
Qiita