StoryboardでSegueによる画面遷移を行う（macOS）

# 概要
- iOSと同じノリで`segue`を作って`show`を設定すると、残念ながらosxでは別ウィンドウで表示されてしまいます。
- 今回は同じウィンドウにて遷移させたいので、実装は下記の通り行います。
 
>[macOS アプリで画面遷移 \(View Controller の切り替え\)](https://qiita.com/morikiyo/items/64fc525cb10fa04eb559)
>今回目標とする画面遷移処理は、最終的に NSView の入れ替えを行えばよくて
> 最低限に必要な処理は以下2点

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

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

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

# 参考
- [macOS アプリで画面遷移 \(View Controller の切り替え\)](https://qiita.com/morikiyo/items/64fc525cb10fa04eb559)
    - 主に参考にしています
- [OS X アプリでStoryboardとSegueを利用する ビューを切り替える編](https://qiita.com/masakihori/items/d9bb915ca2a0918375ae#%E3%81%BE%E3%81%A8%E3%82%81)
- [macOSアプリ用の設定ウィンドウの作成方法 \- Qiita](https://qiita.com/IKEH/items/e11911d01a417b577353)
    - ウィンドウサイズの変更とアニメーションを参考に

# GitHub

- https://github.com/pommdau/Qiita_CustomSegueSample/releases/tag/2020-02-14

# 実装
## Storyboard
- 初期に配置されている`ViewController`に`Container View`を配置します。
- 下記の通り画面全体に広がるように`Constrains`を追加します

![-w705](https://i.imgur.com/JGcu0tv.jpg)

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

![-w1338](https://i.imgur.com/GWIWdoV.jpg)

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

![-w936](https://i.imgur.com/cMotr2B.jpg)

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

![-w417](https://i.imgur.com/th4TUMs.jpg)

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

![-w323](https://i.imgur.com/SNbgRZL.jpg)

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

![-w1409](https://i.imgur.com/PghsMur.jpg)

# FirstViewController / SecondViewController

```swift
@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?)`で実際に使用します

```swift
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"`のように場合分けをします
- 今回は遷移先の`SecondViewController`に`String`を設定し、ウィンドウタイトルを変更しています。
- `SecondViewController`も同じように実装しています。

## SlideSegue
- 今回のメインである`NSStoryboardSegue`のカスタムクラスです

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

### ⑤ アニメーション的にウィンドウを変形する
- 変形後にコンテンツ内容を表示するようにします。
- 設定ウィンドウと同じ実装です。
    - [macOSアプリ用の設定ウィンドウの作成方法 \- Qiita](https://qiita.com/IKEH/items/e11911d01a417b577353)