Help us understand the problem. What is going on with this article?

StoryboardでSegueによる画面遷移を行う(macOS)

概要

  • iOSと同じノリでsegueを作ってshowを設定すると、残念ながらosxでは別ウィンドウで表示されてしまいます。
  • 今回は同じウィンドウにて遷移させたいので、実装は下記の通り行います。

macOS アプリで画面遷移 (View Controller の切り替え)
今回目標とする画面遷移処理は、最終的に NSView の入れ替えを行えばよくて
最低限に必要な処理は以下2点

  • 表示されている View の superview に、次に表示したい View を追加
  • 表示されている View を superview から切り離す

アクションメソッド内に上記処理があれば画面遷移できる
必要に応じてアニメーションをはさめばいい

ちなみに、NSWindow.contentView を入れ替えるのではなく contentView の subview の入れ替えを行う

参考

GitHub

実装

Storyboard

  • 初期に配置されているViewControllerContainer Viewを配置します。
  • 下記の通り画面全体に広がるようにConstrainsを追加します

-w705

  • embedしたViewが全画面に広がるように下記を設定します

-w1338

  • バインディングでEmbedに指定します。

-w936

  • View間のSegueを作成します。
  • ここでShowとすると別ウィンドウで開かれてしまうので、今回はCustomを指定し、コードで遷移処理をゴリゴリ書いていきます。

-w417

  • Segueにはidentifierと後で定義するカスタムクラスを設定します。

-w323

  • 逆方向のSegueも今回設定しておきます。
  • Storyboardでの設定は以上です。

-w1409

FirstViewController / SecondViewController

@IBAction func debugButtonClicked(_ sender: Any) {
    performSegue(withIdentifier: "FirstToSecond", sender: "This is a message from FirstViewController")
}
  • ボタンを押したときにSegueが実行されるようにします。
    • identifierでStoryboard上で作成したSegueを識別しています
    • senderに遷移先へ渡したいオブジェクトを指定します
    • これは次のprepare(for segue: NSStoryboardSegue, sender: Any?)で実際に使用します
override func prepare(for segue: NSStoryboardSegue, sender: Any?) {
    if segue.identifier == "FirstToSecond" {
        let controller = segue.destinationController as! SecondViewController
        if let labelText = sender as? String {
            controller.labelText = labelText
            self.view.window?.title = "SecondView"
        }
    }
}
  • 上記のメソッドはSegueが実行される前に呼ばれます
  • Segueが複数ある場合もあるので、segue.identifier == "FirstToSecond"のように場合分けをします
  • 今回は遷移先のSecondViewControllerStringを設定し、ウィンドウタイトルを変更しています。
  • SecondViewControllerも同じように実装しています。

SlideSegue

  • 今回のメインであるNSStoryboardSegueのカスタムクラスです
class SlideSegue: NSStoryboardSegue {
    override func perform() {
        // ① NSViewControllerの親子関係を設定
        guard
            let sourceViewController = self.sourceController as? NSViewController,             // 遷移前のViewController
            let destinationViewController = self.destinationController as? NSViewController,   // 遷移後のViewController
            let parentViewController = sourceViewController.parent                             // ContainerViewを持つViewController
            else {
                print("downcasting or unwrapping error")
                return
        }

        // ② 遷移先のViewがViewControllerのChildに無いと、ContainerViewに設定できない?
        if (!parentViewController.children.contains(destinationViewController)) {
            parentViewController.addChild(destinationViewController)
        }

        // ③ 遷移後のウィンドウのFrameを計算
        let window = sourceViewController.view.window!
        let contentsViewHeightOffset = sourceViewController.view.frame.height - destinationViewController.view.frame.height
        let titlebarHeight = window.frame.height - sourceViewController.view.frame.height // タイトルバーの高さ
        let newFrame = NSRect(x: window.frame.origin.x,
                              y: window.frame.origin.y + contentsViewHeightOffset,
                              width: destinationViewController.view.frame.width,
                              height: destinationViewController.view.frame.height + titlebarHeight
        )

        sourceViewController.view.superview?.addSubview(destinationViewController.view) // ContainerViewに追加
        sourceViewController.view.removeFromSuperview()                                 // 遷移前のビューを削除

        // ④ 遷移後のViewのConstraintsを設定
        destinationViewController.view.translatesAutoresizingMaskIntoConstraints = false
        destinationViewController.view.leadingAnchor.constraint(equalTo: parentViewController.view.leadingAnchor).isActive = true
        destinationViewController.view.trailingAnchor.constraint(equalTo: parentViewController.view.trailingAnchor).isActive = true
        destinationViewController.view.topAnchor.constraint(equalTo: parentViewController.view.topAnchor).isActive = true
        destinationViewController.view.bottomAnchor.constraint(equalTo: parentViewController.view.bottomAnchor).isActive = true

        // ⑤ アニメーション的にウィンドウを変形する
        destinationViewController.view.isHidden = true   // ウィンドウサイズが変更された後に内容を表示するため
        NSAnimationContext.runAnimationGroup({ _ in
            window.animator().setFrame(newFrame, display: false)
        }, completionHandler: { [weak self] in
            destinationViewController.view.isHidden = false
        })
    }
}

① NSViewControllerの親子関係を設定

  • NSStoryboardSegueクラスのPropertyから、今回使用するViewControllerを取得します。
    • 今回一律ViewControllerでキャストしてしまっているのがあまり良くなさそうではあります…
    • Segue毎にそれぞれNSStoryboardSegueのカスタムクラスを作るのが本筋でしょうか?

② 遷移先のViewがViewControllerのChildに無いと、ContainerViewに設定できない?

  • ここに関する文献が見つからなかったので、そういうことかな?程度の理解です。

③ 遷移後のウィンドウのFrameを計算

  • タイトルバーの位置が変わらない用にy座標を調整します
  • またウィンドウのサイズは遷移先のViewの大きさ(IB上で設定したもの)になるようにしています。
  • ウィンドウのFrameを維持したい場合は、ここを変更してください。

④ 遷移後のViewのConstraintsを設定

  • ContainerViewのConstrainsと同じにすることで、Windowを変化する際コンテンツも変化させるように

⑤ アニメーション的にウィンドウを変形する

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした