How to set custom NSSplitView with NSSplitViewController?

1.9k views Asked by At

I'd like to use a custom NSSplitView with my NSSplitViewController.

The docs say:

To provide a custom split view, set this property at any time before you call super in the inherited viewDidLoad() method; that is, before the split view controller’s isViewLoaded property is true.

My NSSplitViewController subclass is called MainVC.

I tried setting my custom split view in -viewDidLoad before calling [super viewDidLoad]:

- (void)viewDidLoad {
    self.splitView = [MySplitView new];
    [super viewDidLoad];
    // Rest of viewDidLoad...
}

but it didn't work. I got the following error:

2017-09-02 10:35:43.527312-0700 Zee[6497:632581] ** * Assertion failure in -[MainVC setSplitView:], /BuildRoot/Library/Caches/com.apple.xbs/Sources/AppKit/AppKit- 1561/Controllers/NSSplitViewController.m:220

2017-09-02 10:35:43.527558-0700 Zee[6497:632581] MainVC: The -splitView can only be assigned before the view is loaded

I also tried overriding loadView:

- (void)loadView {
    self.splitView = [MySplitView new];
    [super loadView];
}

But I get:

2017-09-02 10:39:39.377345-0700 Zee[6575:639146] ** * -[__NSArrayM objectAtIndex:]: index 0 beyond bounds for empty array

If I do the assignment after calling [super loadView], I get the same error as I did when I tried it in -viewDidLoad.

How do I use a custom NSSplitView in my NSSplitViewController subclass?

4

There are 4 answers

0
Lucas Derraugh On BEST ANSWER

So the index out of bounds issue is related to your split view not having any content rather than something you're doing wrong with initialization. Leaving the initialization in the loadView should be fine. Simply make sure you have initialized your NSSplitViewController subclass with at least 2 NSSplitViewItems before presenting it. Here's an example:

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
    MySplitViewController *vc = [MySplitViewController new];
    vc.splitViewItems = @[
        [NSSplitViewItem splitViewItemWithViewController:[MyViewController new]],
        [NSSplitViewItem splitViewItemWithViewController:[MyViewController new]]
    ];
    self.window.contentViewController = vc;
}
0
eonil On

In my case, this worked. (macOS 10.14, Xcode 10)

final class SplitVC: NSSplitViewController {
    private func patch() {
        let v = NSSplitView()
        v.isVertical = true
        v.dividerStyle = .thin
        splitView = v
        splitViewItems = [
            NSSplitViewItem(viewController: NSTabViewController()),
            NSSplitViewItem(viewController: NSTabViewController()),
        ]
    }

    override init(nibName nibNameOrNil: NSNib.Name?, bundle nibBundleOrNil: Bundle?) {
        super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
        patch()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        patch()
    }
}
1
Ryan McGrath On

The accepted answer in this thread is technically correct, but (understandably) misses a bug in NSSplitViewController that I haven't found documented anywhere. Such is the state of Cocoa development in 2017, I guess...

Anyway, the issue is this: if you're like me and wanted to use an NSSplitViewController with an NSSplitView that starts with just one view, the subclassing approach above won't work and you'll get the indexing error from OP. There's a private method (for drawing the divider) that assumes two NSSplitViewItem's are in there at all times, even though NSSplitView works fine with just one.

What I ended up doing is subclassing both NSSplitViewController and NSSplitView, and providing a check for if I create one that only has one view and swapping in a blank NSViewController instance that gets removed once the loading phase has settled down. I've also found that setting splitViewController.splitViewItems = ... directly doesn't work well, and you should be calling through to addSplitViewItem: to do this - presumably there's some stuff behind the scenes that you miss out on otherwise.

This class is annoyingly not documented, even though it's pretty useful overall. If you're a wayward traveler and you find this, hope it helps.

0
Austin On

I stumbled across what I think is the source of this issue. It occurs in the default implementation of splitView(_ splitView: NSSplitView, shouldHideDividerAt dividerIndex: Int) -> Bool. I think Apple is incorrectly indexing in that method which leads to the out of bounds errors.

To skip calling their implementation, just override it in your NSSplitViewController subclass:

  override func splitView(_ splitView: NSSplitView, shouldHideDividerAt dividerIndex: Int) -> Bool {
    false
  }

If you instead call super.shouldHideDividerAt(...) the errors start happening again:

*** -[__NSArrayM objectAtIndex:]: index 1 beyond bounds [0 .. 0]

The solution others provide of adding "dummy" splitViewItems worked for me, until I added an override for insertSplitViewItem and looked at the .view.frame or .splitViewItems of my NSSplitViewController subclass, then the out-of-bounds errors came back and I was stuck again.

After getting frustrated I went to GitHub to see if I could luck out and find someone working around the bug. I came across this comment/code: https://github.com/MoonfishDeFi/Composite/blob/1ccde807c690739b1739e22f02213e195265a992/cotEditor/Document%20Window/Document%20View/SplitViewController.swift#L62 which has this solution but no explanation.