トップ画像
Popover内のプッシュ遷移に挑戦した話

執筆者: 霽月

最終更新: 2023/03/29

内容

霽月です。Swift初心者向けの記事です。
今回は少し難しめの画面遷移に挑戦してみました。
iOSにはPopover(ポップオーバー)と呼ばれるUIがあり、例えばボタンを押した際に、そのボタンから出る吹き出しのような形でビューを表示することができます。画面の大きいiPadのアプリではよく使われるUIですが、基本的にiPhoneではモーダルで表示されるので、普段iPhoneしか使わない人は見たことがないかもしれません。
今回はそのPopoverの吹き出しの中でプッシュ遷移をやってみようと思います。普通のプッシュ・モーダル遷移などに比べれば割とマイナーな遷移ですが、Apple純正の「時計」「Pages」「Numbers」など、プリインストールのアプリで結構使われてたりします。完成形を先にお見せします。

環境

  • macOS Ventura 13.2.1
  • シミュレータ: iPhone 14 Pro(iOS16.2)
  • Xcode14.2
  • Swift5.7.2


Storyboardを開く

まずはStoryboardを開いて、ViewControllerを選択した状態で「Editor」→「Embed In」→「Navigation Controller」を押す、というお決まりの流れでナビゲーションバーを出します。Storyboardはもうこれ以上使わないので、ViewController.swiftに戻ります。さようなら、Storyboard君。

ポップオーバーを表示

とりあえず、ポップオーバーを表示するボタンを作ってみます。
先ほどナビゲーションバーを用意したのは、UIBarButtonItemを置くためです。ポップオーバーはUIButtonから出るよりUIBarButtonItemから出た方が見慣れてる感じがしたので。まあどっちでも良いですけど。
ということで、できたのがこちらです。

import UIKit

class ViewController: UIViewController, UIPopoverPresentationControllerDelegate {

    override func viewDidLoad() {
        super.viewDidLoad()
        navigationItem.rightBarButtonItem = UIBarButtonItem(
            barButtonSystemItem: .add,
            target: self,
            action: #selector(didTapBarButton)
        )
    }

    @objc func didTapBarButton(_ sender: UIBarButtonItem) {
        let vc = PopoverViewController()
        vc.preferredContentSize = CGSize(width: 300, height: 240)
        vc.modalPresentationStyle = .popover
        let presentationController = vc.popoverPresentationController
        if let sourceView = sender.value(forKey: "view") as? UIView {
            presentationController?.sourceView = sourceView
            presentationController?.sourceRect = sourceView.bounds
        }
        presentationController?.permittedArrowDirections = .up
        presentationController?.delegate = self
        present(vc, animated: true)
    }

    func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle {
        return .none
    }
}

class PopoverViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
    }
}

「+」ボタンを押したらdidTapButtonが呼び出されて、PopoverViewControllerをポップアップで表示します。
ポップアップを出すコードは長いですが、やってることはシンプルです。ポップオーバーのサイズを設定して、ビューのプレゼンテーションスタイルをポップオーバーに設定して、ポップオーバーの吹き出し口を向ける場所と、向きを設定して、最後にpresentで表示します。
記事の冒頭で話したように、本来ポップオーバーはiPhoneではモーダルとして表示されるのですが、presentationControllerのdelegateをselfにしてadaptivePresentationStyle関数で.noneを返すことで、iPhoneでもポップオーバーとして表示されます。

ナビゲーションバーを表示

さて、個人的にここが一番の難関でした。
まず、プッシュ遷移やモーダル遷移を行うにはナビゲーションバーが必須です。
プッシュ遷移を行うコードはこんな感じになっていて、

navigationController?.pushViewController(vc, animated: true)

navigationControllerがnilだったら、プッシュ遷移は不可能なわけです。
だから、PopoverViewControllerにナビゲーションバーを追加する方法を考えねばなりません。

ナビゲーションバーといえばStoryboardで出すものという認識だったので、Storyboardを弄ってPopoverViewControllerにナビゲーションバーをEmbed Inしてみるも、反映されず……。
PopoverViewControllerのviewDidLoadの中で、navigationControllerに新しく生成したUINavigationControllerを代入できないかと考えるも、navigationControllerはゲッターしか定義されていないので代入不可……。
ダメ元でChatGPT君に助けを求めるも、UIViewControllerの、実在しないプロパティを教えられ……w
いや、どうすんのよこれ。
となったところで、一旦考えるのをやめてナビゲーションバーについて情報収集することにしました。
すると、モーダルの画面にナビゲーションバーを追加している動画(リンク)が見つかりました。
動画ではこのようにしてモーダル遷移することで、それを可能にしていました。

        let rootVC = SecondViewController()
        let navVC = UINavigationController(rootViewController: rootVC)
        navVC.modalPresentationStyle = .fullScreen
        present(navVC, animated: true)

なるほど、rootVCではなくnavVCの方をpresentするのか。興味深い。
ということで、こちらも同じやり方でやってみます。

まずはdidTapButton関数の中身を変えていきます。

        let vc = PopoverViewController()
        vc.preferredContentSize = CGSize(width: 300, height: 240)
        let navVC = UINavigationController(rootViewController: vc)
        navVC.modalPresentationStyle = .popover
        let presentationController = navVC.popoverPresentationController
        if let sourceView = sender.value(forKey: "view") as? UIView {
            presentationController?.sourceView = sourceView
            presentationController?.sourceRect = sourceView.bounds
        }
        presentationController?.permittedArrowDirections = .up
        presentationController?.delegate = self
        present(navVC, animated: true)

navVCを宣言して、vcのところをいくつかnavVCに変えただけです。
PopoverViewControllerのviewDidLoadに以下を追加して、ナビゲーションバーに「画面1」と表示します。

        title = "画面1"

実行してみます。
ちゃんとタイトルが表示されました!これで問題は解決です。

遷移ボタンを追加

それでは、PopoverViewControllerにボタンを追加しましょう。
ボタンを宣言して、

    private let button: UIButton = {
        let button = UIButton()
        button.setTitle("遷移する", for: .normal)
        button.backgroundColor = .systemRed
        button.setTitleColor(.white, for: .normal)
        return button
    }()

ボタンを配置するコードをViewDidLoadに書きます。

        button.frame = CGRect(x: 100, y: 100, width: preferredContentSize.width - 200, height: 40)
        view.addSubview(button)

ボタンのイベントを登録して、

        button.addTarget(self, action: #selector(didTapButton), for: .touchUpInside)

何もないただのUIViewControllerに遷移するコードを書きます。

    @objc func didTapButton() {
        let vc = UIViewController()
        vc.view.backgroundColor = .white
        vc.title = "画面2"
        navigationController?.pushViewController(vc, animated: true)
    }

ビューの背景色を設定しないと遷移の時にアニメーションがバグったので、白に設定しています。
これでようやく完成です。対戦ありがとうございました。

最終的なコードの全文

import UIKit

class ViewController: UIViewController, UIPopoverPresentationControllerDelegate {

    override func viewDidLoad() {
        super.viewDidLoad()
        navigationItem.rightBarButtonItem = UIBarButtonItem(
            barButtonSystemItem: .add,
            target: self,
            action: #selector(didTapBarButton)
        )
    }

    @objc func didTapBarButton(_ sender: UIBarButtonItem) {
        let vc = PopoverViewController()
        vc.preferredContentSize = CGSize(width: 300, height: 240)
        let navVC = UINavigationController(rootViewController: vc)
        navVC.modalPresentationStyle = .popover
        let presentationController = navVC.popoverPresentationController
        if let sourceView = sender.value(forKey: "view") as? UIView {
            presentationController?.sourceView = sourceView
            presentationController?.sourceRect = sourceView.bounds
        }
        presentationController?.permittedArrowDirections = .up
        presentationController?.delegate = self
        present(navVC, animated: true)
    }

    func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle {
        return .none
    }
}


class PopoverViewController: UIViewController {
    private let button: UIButton = {
        let button = UIButton()
        button.setTitle("遷移する", for: .normal)
        button.backgroundColor = .systemRed
        button.setTitleColor(.white, for: .normal)
        return button
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        title = "画面1"
        
        button.frame = CGRect(x: 100, y: 100, width: preferredContentSize.width - 200, height: 40)
        view.addSubview(button)

        button.addTarget(self, action: #selector(didTapButton), for: .touchUpInside)
    }

    @objc func didTapButton() {
        let vc = UIViewController()
        vc.view.backgroundColor = .white
        vc.title = "画面2"
        navigationController?.pushViewController(vc, animated: true)
    }
}
取得に失敗しました

2022年度 入部

Twitter GitHub