LoginSignup
4
2

More than 3 years have passed since last update.

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

Posted at

概要

  • 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を変化する際コンテンツも変化させるように

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

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2