📝

NSSplitViewControllerで3panesパターンを実装する

2023/03/12に公開

UIKitではUISplitViewControllerを使って2もしくは3カラムのUIを実装しますが、iPadで最適なのはあくまでprimary-(supplementary)-secondaryのパターンであって、inspector paneはより拡張性の高いmac app特有のパターンです。

この記事ではNSSplitViewControllerを利用して、inspector paneを伴った3panesパターンの実装方法を順をおって解説していきます。

1. プロジェクトの作成

「macOS > App」のテンプレートを選択し、プロジェクトを作成します。

スクリーンショット 2023-01-12 16.53.23.png

この記事ではStoryboardとAppKitを使うので、Interface>Storyboardと選択して作成してください。
(今はCoreDataもUnitTestもいらないのでオプションは外してください。)

2. コンポーネントの配置

Storyboardを開き、初期配置されているWindow SceneとView Controller Sceneを範囲選択して削除してください。そして、cmd+shift+Lでライブラリを開き、"Window Controller with Sidebar"というコンポーネントをドロップします。ドロップされたWindow Controllerの"Is Initial Controller"のチェックを入れておきます。

スクリーンショット 2023-01-12 17.02.59.png

3. Toolbarにアイテムを追加して属性を設定

Storyboard上でToolbarをクリックするとパネルが表示されて、配置するToolbarItemを決めることができます。"Default Toolbar Items ... drag in items from above"と書いてあるところに入れたものがアプリの起動時にデフォルトで表示されます。

まずはflexible space以外のアイテムをドロップして削除します。その後、ライブラリからToolbarItemをAllowed Toolbar Itemsに一度ドロップして、そこからDefault Toolbar Itemsにドロップします。Allowed Toolbar ItemsのToolbar Itemをクリックして、インスペクタペーンからImage Nameにsidebar.rightを設定します。
スクリーンショット 2023-01-12 17.07.07.png

ついでに、Toolbarをクリックした状態で、インスペクタからDisplay > Icon Onlyを設定しておきます。

4. 3つ目のPaneを追加

ライブラリからView Controllerを1つ追加でドロップします。Split View Controller Sceneをクリックしてctrlキーを押しながらドラッグして追加したView Controllerの上で放し、split viewsのコネクションを作成します。

スクリーンショット 2023-01-12 17.18.45.png

このとき、Split View Controller Sceneの一番右端のNSSplitViewItemはHodling Priorityを260に上げておき、User Can Collapseのチェックを入れておきます。

Holding Priorityはサイズを保持する優先度ですが、例えばウインドウの端を持ってサイズを大きくしたときにサイドバーやインスペクタが大きくなると変なので、コンテンツエリアのみ拡大縮小してほしいです。左右のHolding Priorityは260など同一に設定し、中央だけ低い値に設定しておけばその挙動を実現できます。

5. 適当なビューを配置してAutolayoutを設定

View Controller Sceneの3つに対して適当なビューをそれぞれ載せて、上下左右0ptの制約を付けます。ここではPlain Document Content Text Viewを使い、適当なテキストを設定しておきます。

その後、sidebarとinspectorに該当するpane上のテキストビューには、それぞれ"width >= 250"の幅制約を付け、content areaに該当するpaneのテキストビューには"width >= 300"の幅制約を付けておきます(値はプロジェクトごとによります)

NSSplitViewControllerのドキュメントを読むと、SplitViewの仕組みはあったとしても、子ビューでAutolayout制約を設定する必要があると言っています。

To use a split view controller, you must use Auto Layout for the child views and to support animations that collapse and reveal child views. For example, if you design a layout that contains two views, a content area and an optional sidebar, you employ Auto Layout constraints to specify whether the content area shrinks or remains the same size when the sidebar becomes visible.
(https://developer.apple.com/documentation/appkit/nssplitviewcontroller)

5. コードでアクションを設定

NSSplitViewControllerのサブクラスを作成し、StoryboardのSplit View Controller SceneのCustom Classとして設定しておきます。inspectorのIBOutletを繋いでおきます。

SplitViewController.swift
import Cocoa

class SplitViewController: NSSplitViewController {

    @IBOutlet weak var inspectorSplitViewItem: NSSplitViewItem!

}

NSWindowControllerのサブクラスを作成し、StoryboardのWindow SceneのCustom Classとして設定しておきます。インスペクタトグル用のToolbar ItemからIBAction func toggleInspectorPane(sender:Any?)を接続しておきます。

アニメーション用のプロキシオブジェクトを経由しつつisCollapsedプロパティをトグルします。

WindowController.swift
import Cocoa

class WindowController: NSWindowController {

    override func windowDidLoad() {
        super.windowDidLoad()
    
        // Implement this method to handle any initialization after your window controller's window has been loaded from its nib file.
    }

    @IBAction func toggleInspectorPane(_ sender: Any) {
        (contentViewController as! SplitViewController).inspectorSplitViewItem.animator().isCollapsed.toggle()
    }
    
}

完成!

これで狙ったインスペクタの挙動が実現できました。サンプルのgifを上げておきます。

画面収録 2023-01-12 18.05.07.gif

概ねこのような形で進め、あとはそれぞれのオブジェクトのプロパティを調節したりなどで見た目などを好きなように変えていきます。

おまけ

以下のコードのように.sidebarTrackingSeparatorを活用すると、サイドバーペーン内にNSToolbarItemを置くことができ、トグルするとタイトルバーの左に位置するような挙動を実現できます。コードで書いてます。(このコード単体では動きません。参考程度にお考えください)


extension NSToolbarItem.Identifier {
    static let leadingPaneToggleItem = NSToolbarItem.Identifier("jp.co.raiso.tweetboxproto4.toolbaritem.toggle-leading-pane")
    static let trailingPaneToggleItem = NSToolbarItem.Identifier("jp.co.raiso.tweetboxproto4.toolbaritem.toggle-trailing-pane")
}

extension ToolbarDelegate: NSToolbarDelegate {

    func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
        return [
            .leadingPaneToggleItem,
            .sidebarTrackingSeparator,
            .flexibleSpace,
            .trailingPaneToggleItem
        ]
    }

    func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
        return toolbarDefaultItemIdentifiers(toolbar)
    }

    func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? {
        var toolbarItem: NSToolbarItem?

        switch itemIdentifier {
        case .leadingPaneToggleItem:
            let item = NSToolbarItem(itemIdentifier: itemIdentifier)
            item.image = NSImage(systemSymbolName: "sidebar.left", accessibilityDescription: nil)
            item.action = #selector(ViewController.toggleLeadingPane(sender:))
            toolbarItem = item

        case .trailingPaneToggleItem:
            let item = NSToolbarItem(itemIdentifier: itemIdentifier)
            item.image = NSImage(systemSymbolName: "sidebar.right", accessibilityDescription: nil)
            item.action = #selector(ViewController.toggleTrailingPane(sender:))
            toolbarItem = item

        case .toggleSidebar:
            let item = NSToolbarItem(itemIdentifier: .toggleSidebar)
            toolbarItem = item

        case .sidebarTrackingSeparator: //これ
            let item = NSToolbarItem(itemIdentifier: .sidebarTrackingSeparator)
            toolbarItem = item

        default:
            toolbarItem = nil

        }
        toolbarItem?.isBordered = true

        return toolbarItem
    }

}

20230112-191406.png

GitHubで編集を提案

Discussion