19

I want to replace RBSplitView with NSSplitView in my existing project. The application is now leopard only and I would like to replace RBSplitView with the new NSSplitView shipped with Leopard.

However, I'm missing RBSplitView's handy methods expand and collapse in NSSplitView. How can I expand and collapse parts of NSSplitView programmatically?

Cœur
  • 37,241
  • 25
  • 195
  • 267
cocoafan
  • 4,884
  • 4
  • 37
  • 45

11 Answers11

31

Simply hide the subview you want to collapse, e.g.

[aSubViewToCollapse setHidden:YES];

You might also want to implement the delegate method -(BOOL)splitView:shouldHideDividerAtIndex: to return YES to hide the divider when a collapsed.

Andreas Järliden
  • 1,884
  • 1
  • 17
  • 16
  • 3
    +1 This should be the answer which actually worked as I expected. `-[NSSplitView isSubviewCollapsed:]` returns `YES` if the subview is hidden. – eonil Jul 08 '13 at 01:25
  • 3
    Anyway you need to call `-[NSSplitView adjustSubviews]` to update graphics right after setting the hidden. – eonil Jul 08 '13 at 01:29
  • This method works, but it has the weird side effect of making the view that I hid smaller when I set the hidden to NO again – Will Mar 11 '14 at 20:52
  • 1
    To resolve the weird side-effect @Will refers to (hidden view comes back narrower), set the divider position explicitly after the view is un-hidden. – Michael Teper Jan 21 '15 at 18:38
  • 1
    I just tested this solution on OS X 10.11 and it shows the result I expected: the subview of the split view are being hidden, without any change in layout or divider position. So this is not a solution to the problem. – osxdirk Mar 20 '16 at 19:06
10

I just got programmatic expanding and collapsing of NSSplitView to work. I've also configured my NSSplitView to expand/collapse a subview whenever the divider is double-clicked, so I wanted this to play nice with that feature (and it seems to). This is what I did:

(in this example, splitView is the NSSplitView itself, splitViewSubViewLeft is the subview I wish to expand/collapse and lastSplitViewSubViewLeftWidth is an instance variable of type CGFloat.)

// subscribe to splitView's notification of subviews resizing
// (I do this in -(void)awakeFromNib)
[[NSNotificationCenter defaultCenter]
 addObserver:self
 selector:@selector(mainSplitViewWillResizeSubviewsHandler:)
 name:NSSplitViewWillResizeSubviewsNotification
 object:splitView
 ];

// this is the handler the above snippet refers to
- (void) mainSplitViewWillResizeSubviewsHandler:(id)object
{
    lastSplitViewSubViewLeftWidth = [splitViewSubViewLeft frame].size.width;
}

// wire this to the UI control you wish to use to toggle the
// expanded/collapsed state of splitViewSubViewLeft
- (IBAction) toggleLeftSubView:(id)sender
{
    [splitView adjustSubviews];
    if ([splitView isSubviewCollapsed:splitViewSubViewLeft])
        [splitView
         setPosition:lastSplitViewSubViewLeftWidth
         ofDividerAtIndex:0
         ];
    else
        [splitView
         setPosition:[splitView minPossiblePositionOfDividerAtIndex:0]
         ofDividerAtIndex:0
         ];
}
hasseg
  • 6,787
  • 37
  • 41
  • 2
    This doesn't actually "collapse" the subview. It just shrinks it to 0. This is a problem because `-isSubviewCollapsed:` still always returns `NO` and, if your subview's interface had any autoresizing masks applied, they get messed up. See the solutions below that involve using `-setHidden:` for the proper answer. – jemmons Jul 16 '11 at 14:12
  • 3
    The method `setPosition:ofDividerAtIndex:` does not shrink the subview to a zero size. In the code above, the author sets the new position to a fixed value (`lastSplitViewSubViewLeftWidth`), but one could have restored the pre-collapsed value with `setPosition:NSMaxX(subview.frame) ofDividerAtIndex:0`. Note that since the subview retains its original width, no notification is needed to store it. See also: http://cocoadev.com/wiki/NSSplitView – Demitri Nov 20 '12 at 16:51
  • This solution doesn't work when I try to collapse the right split view pane by using index 1. I'm sure there is something that can make it work but it's not obvious, Andreas Järliden solution worked perfectly. – Brad G Apr 05 '14 at 17:45
7

In El Capitan, this did the trick for me.

splitViewItem.collapsed = YES;
Daniel Nordh
  • 600
  • 6
  • 9
  • 1
    What is `splitViewItem`? This seems promising but it doesn't say what to actually do or how to actually do it – Ky - Nov 18 '16 at 22:09
  • 1
    NSSplitViewItem is a class used by NSSplitViewController, which simplifies collapse but is more complicated to set-up. – Giles May 21 '19 at 08:52
7

I tried the solution above, and found it did not work, as isSubviewCollapsed never returned YES

A combination of the suggestions yielded a result which works

if ([splitViewTop isHidden]) {
    [splitViewTop setHidden:NO];
    [split
     setPosition:previousSplitViewHeight
     ofDividerAtIndex:0];
}
else {
    [splitViewTop setHidden:YES];
}
[split adjustSubviews];
Milliways
  • 1,265
  • 1
  • 12
  • 26
  • Changing isSubviewCollapsed to isHidden is IMO a design failure. One will get some reading difficulties in future if he is not familiar with your problem. If definitely suggest hasseg's solution. – cocoafan Jul 11 '11 at 11:07
  • 3
    The confusion here seems to center around the definition of "collapse". Does collapsing a view mean setting its size to 0, or hiding it. NSSplitViewDelegate Protocol gives this definition in splitView:canCollapseSubview: "A collapsed subview is hidden but retained by the split view object, with the same size it had before it was collapsed." So, @jemmons and Milliways appear to be in the right of it. – Elise van Looij Oct 04 '11 at 11:29
3

After some experimenting with the suggestions this was the easiest solution I found:

-(void)toggleCollapsibleView:(ib)sender {
   [collapsibleView setHidden:![splitView isSubviewCollapsed:collapsibleView]];
   [splitView adjustSubviews];
}

The function is a user defined first-responder action. It is triggered by a menu-item (or keystroke). The collapsibleView is a subview in the splitView both of which are connected in IB with their properties.

BalancingRock
  • 2,686
  • 1
  • 12
  • 10
3

In macOS Sierra, the collapsed property is changed to isCollapsed. Is straight forward just setting the property to true or false. The following code is from my WindowController, where I have two SplitViewItems.

@IBAction func toggleMap(_ sender: Any) {
    if let splitViewController = contentViewController as? NSSplitViewController {
        let splitViewItem = splitViewController.splitViewItems
        if splitViewItem.first!.isCollapsed {
            splitViewItem.first!.isCollapsed = false
        } else if splitViewItem.last!.isCollapsed {
            splitViewItem.last!.isCollapsed = false
        } else {
            if splitViewItem.first!.isCollapsed {
                splitViewItem.first!.isCollapsed = false
            }
            splitViewItem.last!.isCollapsed = true
        }
    }
}
GJ Nilsen
  • 695
  • 9
  • 27
2

NSSplitView actually has a private method -(void)_setSubview:(NSView *)view isCollapsed:(BOOL)collapsed that does this. Those who would like to ignore all warnings against using private methods, behold:

- (void)toggleSubview:(NSView *)view {
    SEL selector = @selector(_setSubview:isCollapsed:);
    NSMethodSignature *signature = [NSSplitView instanceMethodSignatureForSelector:selector];
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
    invocation.target = self;
    invocation.selector = selector;
    [invocation setArgument:&view atIndex:2];
    BOOL arg = ![self isSubviewCollapsed:view];
    [invocation setArgument:&arg atIndex:3];
    [invocation invoke];
}

I implemented this as a category on NSSplitView. The only issue is that Xcode gives a warning about _setSubview:isCollapsed: being undeclared... I'm not really sure how to get around that.


El Capitan Update

I haven't written any code for OS X in ~2 years now so I haven't been able to verify this, but according to lemonmojo in the comments below, _setSubview:isCollapsed: was renamed in El Capitan to _setArrangedView:isCollapsed:.

Community
  • 1
  • 1
ArtOfWarfare
  • 20,617
  • 19
  • 137
  • 193
  • I'll update to let you know if this can fly on the Mac App Store or not. I know Apple has rules against using private methods, but with Obj-C being such a dynamic language I suspect there must be a way (possibly my own code in this answer accomplishes it) to dodge their checker. – ArtOfWarfare Nov 17 '13 at 06:12
  • 1
    Although this is technically a private method, I suspect that it probably is the best solution for every version of OS X–past, present, and future. Up until Apple makes a public method to accomplish this same thing, of course. – ArtOfWarfare Nov 17 '13 at 06:17
  • 1
    I previously used this private method but it seems like it has been removed in El Capitan. respondsToSelector: now returns NO and calling the method anyway results in an error. – lemonmojo Oct 06 '15 at 07:43
  • @lemonmojo - That's too bad. You could try printing out all of the methods of the class... I haven't used Obj-C in awhile so I don't recall exactly how you do that... But you could do that, maybe even in a Playground in Xcode, to determine if maybe Apple has added some new methods that allow you to programmatically expand/collapse sub views. – ArtOfWarfare Oct 06 '15 at 13:08
  • 2
    Good point! I actually went ahead and fixed the issue by manually adjusting the frames of the subviews. But if anyone is looking for a solution similar to using "_setSubview:isCollapsed:", "_setArrangedView:isCollapsed:" seems to do the trick on El Capitan. – lemonmojo Oct 06 '15 at 13:18
2

In swift this works

func togglePanel() {
    let splitViewItem = self.mySplitView.arrangedSubviews

    if mySplitView.isSubviewCollapsed(outline.view){
        splitViewItem[0].hidden = false
    } else {
        splitViewItem[0].hidden = true
    }

call this from IBAction, outline is an OutlineViewController with own xib and we need the view hence outline.view, keeping it simple but hope you get the idea

@IBAction func segmentAction(sender: NSSegmentedControl) {
    splitVC?.togglePanel(sender.selectedSegment)
}

and

func togglePanel(segmentID: Int) {
    let splitViewItem = self.mySplitView.arrangedSubviews

    switch segmentID {

    case segmentID:
        if mySplitView.isSubviewCollapsed(splitViewItem[segmentID]) {
            splitViewItem[segmentID].hidden = false
        } else {
            splitViewItem[segmentID].hidden = true
        }
    default:
        break
    }

}

And implement delegate

func splitView(splitView: NSSplitView, shouldHideDividerAtIndex dividerIndex: Int) -> Bool {
    return true
}

And with 10.11 you might just use toggleSidebar action method. How to toggle visibility of NSSplitView subView + hide Pane Splitter divider? https://github.com/Dis3buted/SplitViewController

Peter Ahlberg
  • 1,359
  • 2
  • 24
  • 26
  • `toggleSidebar(_:)` belongs to `NSSplitViewController`, not `NSSplitView`. So it's only a solution to the problem if you're willing (and able) to use that. – bfx Oct 10 '17 at 10:02
  • That's right of course. However you can use `NSSplitView` on its own (like I am doing right now) and then you don't have the `NSSplitViewController` API available. The OP's question says nothing about the controller, it's all about `NSSplitView`. – bfx Oct 10 '17 at 10:32
  • That's a nice touch and also a great API to use. It's just not clear where to look for it since it's not on the view itself but on a whole other class. It's also not clear from the original post you linked to. Both questions don't mention the controller, also the tags only say `NSSplitView`. So unless you're already set up with a `NSSplitViewController` when you're coming here, your add-on is not a solution, nor does it provide an easy way to find one. – bfx Oct 10 '17 at 10:44
1

I recommend to use NSSplitViewController instead, and NSSplitViewItem.isCollapsed to control them. This just work.

let item: NSSplitViewItem = ...
item.isCollapsed = true

To make this to work properly, you have to configure split-UI components with mainly view-controllers. Otherwise, it can be broken.

eonil
  • 83,476
  • 81
  • 317
  • 516
0
@IBOutlet weak var horizontalSplitView: NSSplitView!
var splitViewItem : [NSView]?
var isSplitViewHidden: Bool = false

override func viewDidLoad() {
    super.viewDidLoad()
    // To Hide Particular Sub-View.
    splitViewItem = self.horizontalSplitView.arrangedSubviews
    splitViewItem?[0].isHidden = true
    isSplitViewHidden = true
}

//MARK: View / Manage All Jobs Button Tapped.
@IBAction func actionManageScheduleJobsButtonTapped(_ sender: Any) {
    if isSplitViewHidden == true {
        isSplitViewHidden = false
        splitViewItem?[0].isHidden = false
    } else {
        isSplitViewHidden = true
        splitViewItem?[0].isHidden = true
    }
}

--------- OR ----------

//MARK: View / Manage All Jobs Button Tapped.
@IBAction func actionManageScheduleJobsButtonTapped(_ sender: Any) {
    if splitViewItem?[0].isHidden == true {
        splitViewItem?[0].isHidden = false
    } else {
        splitViewItem?[0].isHidden = true
    }
}
Mannam Brahmam
  • 2,225
  • 2
  • 24
  • 36
0

You could try Brandon Walkin's BWToolKit.

The BWSplitView class has a method

- (IBAction)toggleCollapse:(id)sender;
Abizern
  • 146,289
  • 39
  • 203
  • 257
  • 1
    BWToolKit has very nice controls. Nothing against the plugin. But I thought `NSSplitView` is now able to do all the tricks that `RBSplitView` can? – cocoafan May 29 '09 at 11:28