UIViewでUIImagePickerControllerを呼ぶ

UIImagePickerControllerをUIViewControllerから表示させる方法について記述している記事は多くありましたが、UIViewからそれを行う記事があまり見当たらなかったので、今回記事を書いてみることにしました。

UIViewからUIImagePickerを表示させること自体がレアなケースだと思いますが、参考になれば幸いです。

<h2>動作イメージ</h2>

![動作2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1745924/fa8ffbee-ab1c-ab50-4e22-e76faa66a916.gif)



<h2>なぜそれが必要になったのか</h2>
今回とあるアプリを作成している中で、UIViewControllerに UIStackViewとUICollectionView が置いてあり、UICollectionViewのそれぞれのCellをタップすると、別途作成しているUIViewがinsertされるといった実装を行っておりました。

なぜこのような実装について語ると長くなりますが、簡単に言えば異なるUIViewを画面遷移せずにViewControllerで表示したかった為です。
そんな中、あるUIViewでユーザーから写真を入力してもらう必要性がありました。
そこで、自分なりの正解に辿り着いたので、以下に記述します。

<h2>必要なもの</h2>

・UIImagePickerController
・UIPopoverPresentationControllerDelegate
・UIApplicationのExtension（最前面のViewControllerを判定する）
・UIImagePickerControllerDelegate（タップされた写真を判定）
・UINavigationControllerDelegate（タップされた写真を通知）


<h2>必要な処理</h2>

:::note warn
この実装にはUIViewで画面遷移を行う方法と、UIViewからViewControllerにタップイベントを通知して、VC側で画面遷移を行う方法があります。
UIView側で画面遷移を行うのは、少し愚直的であり、設計的にも良く無いと思いますが、今回は両方ご紹介します。
:::

<h3>1. UIViewで画面遷移を行う方法</h3>

まずは、UIimagePickerControllerのインスタンスを作成します。

この中で、souceTypeや遷移の方法を設定します。
5行目については、```.any```でも問題ありません。

```Swift
private let imagePickerView: UIImagePickerController = {
        let picker = UIImagePickerController()
        picker.sourceType = .photoLibrary
        picker.modalPresentationStyle = .popover
        picker.popoverPresentationController?.permittedArrowDirections = .up
        return picker
    }()
```

次に、それを呼ぶためのトリガーとなるもの（今回はUIButton）を設定し、タップアクション等によって、以下のアクションを行うようにします。

```Swift
@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に当てはめてる処理になりますので、説明は省きます。

<h4>UIView側で画面遷移する際のコードの全体像</h4>
動作確認済みです。

StoryBoardを使用しない実装を行い、制約については[SnapKit](https://github.com/SnapKit/SnapKit)を使用しています。
レイアウトについてはあくまでサンプルアプリである為、その点ご容赦ください。

```Swift:HogeUIView.swift
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)
    }
}
```

```Swift:ViewController.swift
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()
        }
    }
}
```

```Swift:UIApplication+.swift
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
    }
}
```

<h3>2. UIViewのタップイベントをVCに通知して、VCで画面遷移を行う方法</h3>

こちらについては、どのようにタップイベントをVCに通知すべきかについてのみ解説します。
その中で行うUIImagePicker等の表示・遷移のコードが重複するためです。

今回はRxSwiftを使って通知処理を書いています。
```Swift:HogeView.swift (UIView側)
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
  }()
```

```Swift: ViewController.swift
class ViewController: UIViewController {

//これをViewDidLoad内で呼んでください。
func bind() {
   customView.buttonDidTap
   //customViewはUIViewをインスタンス化したものです
     .subscribe(onNext: { [weak self] _ in
        self.onTapedButton()//タップされた際の処理
    })
    .disposed(by: disposeBag)
}
```
あとは、UIView側で画面遷移を行うコード内の```delegete```の記述やタップアクションをこちらに移行することで、動作します。
さらに、こちらの場合は```UIApplication```のExtensionは不要となり、

```diff_Swift
- UIApplication.topViewController()?.dismiss(animated: true)
+ present(imagePickerController, animated: true, completion: nil)
```
このように記述することができます。

※UIView側でsetupView()で記述している```imagePickerView.delegate = self```もVC側に書くことを忘れないように注意ください。


<h2>最後に</h2>

今回の実装で、お役に立てれば幸いです。
また、おそらくこれ以外の実装方法も多くあると思いますので参考までにお願いします。


<h2>参考文献</h2>

Apple Developer - UIImagePicker

https://developer.apple.com/documentation/uikit/uiimagepickercontroller



Apple Developer - UIPopoverPresentationController

https://developer.apple.com/documentation/uikit/uipopoverpresentationcontroller

StackOverFlow

https://stackoverflow.com/questions/16446188/ios-uiimagepicker-in-a-uiview-subview

Qiita

https://qiita.com/orimomo/items/1a44337de974a72b6266