概要
- iOSと同じノリで
segue
を作ってshow
を設定すると、残念ながらosxでは別ウィンドウで表示されてしまいます。 - 今回は同じウィンドウにて遷移させたいので、実装は下記の通り行います。
macOS アプリで画面遷移 (View Controller の切り替え)
今回目標とする画面遷移処理は、最終的に NSView の入れ替えを行えばよくて
最低限に必要な処理は以下2点
- 表示されている View の superview に、次に表示したい View を追加
- 表示されている View を superview から切り離す
アクションメソッド内に上記処理があれば画面遷移できる
必要に応じてアニメーションをはさめばいい
ちなみに、NSWindow.contentView を入れ替えるのではなく contentView の subview の入れ替えを行う
参考
-
macOS アプリで画面遷移 (View Controller の切り替え)
- 主に参考にしています
- OS X アプリでStoryboardとSegueを利用する ビューを切り替える編
-
macOSアプリ用の設定ウィンドウの作成方法 - Qiita
- ウィンドウサイズの変更とアニメーションを参考に
GitHub
実装
Storyboard
- 初期に配置されている
ViewController
にContainer View
を配置します。 - 下記の通り画面全体に広がるように
Constrains
を追加します
-
embed
したViewが全画面に広がるように下記を設定します
- バインディングで
Embed
に指定します。
- View間のSegueを作成します。
- ここで
Show
とすると別ウィンドウで開かれてしまうので、今回はCustom
を指定し、コードで遷移処理をゴリゴリ書いていきます。
-
Segue
にはidentifier
と後で定義するカスタムクラスを設定します。
- 逆方向の
Segue
も今回設定しておきます。 - Storyboardでの設定は以上です。
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"
のように場合分けをします - 今回は遷移先の
SecondViewController
にString
を設定し、ウィンドウタイトルを変更しています。 -
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を変化する際コンテンツも変化させるように
⑤ アニメーション的にウィンドウを変形する
- 変形後にコンテンツ内容を表示するようにします。
- 設定ウィンドウと同じ実装です。