20

With auto layout enabled, auto saving divider positions by setting an autosave name for the NSSplitView in interface builder results in each divider being completely collapsed on an app restart. Disabling auto layout allows auto save works perfectly.

I have tried this in a new Xcode project as well, same result. Is this a bug, or a known incompatibility?

How could I work around this (or is there a fix to this, if it is a bug)?

Jordan Smith
  • 10,310
  • 7
  • 68
  • 114
  • It works fine in a new project for me. Have you given the split view an autosave name? How are you stopping the program when testing? Using Cmd-Q, or the stop button in XCode? – Steve Waddicor May 16 '13 at 12:29

8 Answers8

36

I found that setting Identifier and Autosave within a Storyboard with autolayout enabled doesn't work. However it did work for me once I set the autosaveName programatically.

class MySplitViewController: NSSplitViewController {

    override func viewDidLoad() {
        super.viewDidLoad()            
        splitView.autosaveName = "Please Save Me!"
    }
}

        
csch
  • 1,455
  • 14
  • 12
  • This solution worked perfect for me and much simpler than the above workarounds. – Tap Forms Aug 06 '15 at 16:44
  • 1
    This was the one for me too! I want to reuse the MySplitViewController for different splitviews on the storyboard and found that the value I enter for each splitviewcontroller in the storyboard in 'Identity Inspector'-> 'Restoration ID' surfaces in the member-variable called 'identifier' in MySplitViewController. You can assign it to the autosave value like above by saying: "splitView.autosaveName = identifier" – Hans Aug 22 '16 at 09:27
  • Wow, that's a great answer! – Chrstpsln Oct 01 '17 at 14:43
  • 4
    For Swift 4 it's: `splitView.autosaveName = NSSplitView.AutosaveName(rawValue: "Please Save Me! Really!")` – Lupurus Nov 18 '17 at 23:06
  • A tangentially related fact is that a system user preference can prevent split view autosave from working: https://github.com/insidegui/WWDC/issues/404 – allenh May 31 '18 at 21:08
  • Wow. This seems way too simple to be true. I walked into this thinking I'd have to observe the `NSSplitViewController`'s pane widths, save them to `UserDefaults` and check for their values when my app launched. This "just works"... unlike most things in AppKit. Thanks! – Clifton Labrum Aug 16 '19 at 01:27
  • 3
    In Swift 5 it's back to `splitView.autosaveName = "Please Save Me!"` – Giles Apr 16 '20 at 11:28
13

I ran into this problem as well, and I found that I need to set both the identifier and the autosaveName values for the NSSplitView, and that they need to be set to different values.

marcprux
  • 9,845
  • 3
  • 55
  • 72
  • 1
    I've been struggling with this for a while and finally found your answer. Can't believe it was something silly like the identifier. But setting it to something different than my auto save name worked a treat. Thanks! – Tap Forms Dec 01 '13 at 23:57
  • What do you mean by `identifier`? Where does this property defined? – Konstantin Pavlikhin Aug 26 '14 at 14:52
  • @KonstantinPavlikhin `NSUserInterfaceItemIdentification`, most (all?) views implement this. –  Apr 08 '19 at 17:26
  • By the way, makes no difference, split view state is not saved. –  Apr 08 '19 at 17:28
11

Depending on your case, the view might not be in a view hierarchy when you first instanciate it. If that is the case, the autosaveName will only work if it is set AFTER the view has been added to the windows view hierarchy, so you might consider setting your autosave name in

func viewDidMoveToWindow() {
    super.viewDidMoveToWindow()
    splitView.autosaveName = "mySplitViewState"
}
  • From what I tested, THIS should be the accepted answer. Maybe not perfect, but if you place it in the "func viewWillLayout()" function, it works. – JackPearse Oct 10 '19 at 15:53
  • That's the correct answer! You can also wait for `window` property to change: `controller.view.observe(\.window) { ... }` – Wojciech Kulik Jun 12 '22 at 16:17
4

For me, setting identifier + autosavename didn't work. I had to fall back on the solution provided by ElmerCat. However I slightly modified the code to avoid setting the divider position (didn't get it working). Instead, i'm modifying the view size. I also added hiding of collapsed views.

@interface NSSplitView (RestoreAutoSave)
- (void)restoreAutosavedPositions;
@end

@implementation NSSplitView (RestoreAutoSave)
- (void)restoreAutosavedPositions
{
    NSString *key = [NSString stringWithFormat:@"NSSplitView Subview Frames %@", self.autosaveName];
    NSArray *subviewFrames = [[NSUserDefaults standardUserDefaults] valueForKey:key];

    // the last frame is skipped because I have one less divider than I have frames
    for( NSInteger i = 0; i < subviewFrames.count; i++ ) {

        if( i < self.subviews.count ) { // safety-check (in case number of views have been removed while dev)

            // this is the saved frame data - it's an NSString
            NSString *frameString = subviewFrames[i];
            NSArray *components = [frameString componentsSeparatedByString:@", "];

            // Manage the 'hidden state' per view
            BOOL hidden = [components[4] boolValue];
            NSView* subView =[self subviews][i];
            [subView setHidden: hidden];

            // Set height (horizontal) or width (vertical)
            if( !self.vertical ) {

                CGFloat height = [components[3] floatValue];
                [subView setFrameSize: NSMakeSize( subView.frame.size.width, height ) ];
            }
            else {

                CGFloat width = [components[2] floatValue];
                [subView setFrameSize: NSMakeSize( width, subView.frame.size.height ) ];
            }
        }
    }
}
  • Bugs in NSSplitView still make this necessary, and this fix still works. Thanks. This should be the accepted answer. The problem with requiring `identifier` to be set appears to have been fixed by Apple. – bhaller Feb 09 '16 at 18:02
2

NSSplitView is notorious for being particularly fussy and troublesome; you sometimes have to go out of your way to make it behave properly. I knew my settings were being saved in User Defaults - I could see them change correctly via the Terminal "Defaults read etc...", but they weren't getting restored when the application reopened.

I solved it by manually reading the saved values and restoring the divider positions during awakeFromNib.

Here's a Category on NSSplitView that politely asks it to please set its divider positions to their autosaved values:

@interface NSSplitView (PraxCategories)
- (void)restoreAutosavedPositions;
@end

@implementation NSSplitView (PraxCategories)
- (void)restoreAutosavedPositions {

    // Yes, I know my Autosave Name; but I won't necessarily restore myself automatically.
    NSString *key = [NSString stringWithFormat:@"NSSplitView Subview Frames %@", self.autosaveName];

    NSArray *subviewFrames = [[NSUserDefaults standardUserDefaults] valueForKey:key];

    // the last frame is skipped because I have one less divider than I have frames
    for (NSInteger i=0; i < (subviewFrames.count - 1); i++ ) {

        // this is the saved frame data - it's an NSString
        NSString *frameString = subviewFrames[i];
        NSArray *components = [frameString componentsSeparatedByString:@", "];

        // only one component from the string is needed to set the position
        CGFloat position;

        // if I'm vertical the third component is the frame width
        if (self.vertical) position = [components[2] floatValue];

        // if I'm horizontal the fourth component is the frame height
        else position = [components[3] floatValue];

        [self setPosition:position ofDividerAtIndex:i];
    }
}
@end

Then just call the method during awakeFromNib for each NSSplitView you wish to restore:

for (NSSplitView *splitView in @[thisSplitView, thatSplitView, anotherSplitView]) {
    [splitView restoreAutosavedPositions];
}
ElmerCat
  • 3,126
  • 1
  • 26
  • 34
1

I found that using NSSplitView is terrible in auto layout mode. So I wrote autolayout-based split view: https://github.com/silvansky/TwinPanelView

It can store its handle position (not fully automized).

silvansky
  • 2,426
  • 2
  • 19
  • 20
1

Found myself looking at the same old NSSplitView autosave issues from years back now with Mac OS 10.12 . Happily Joris' solution is still a great workaround. Here it is a tested Swift 3 extension that works fine in our current project.

Note: Since Auto Layout apparently overrides the autosave defaults after awakeFromNib in the NSSplitView restoreAutoSavePositions() needs to be called in viewDidLoad or thereabouts of the view controller to get this working.

extension NSSplitView {

    /*
    ** unfortunately this needs to be called in the controller's viewDidAppear function as
    ** auto layout kicks in to override any default values after the split view's awakeFromNib
    */
    func restoreAutoSavePositions() {

        let key = String(format: "NSSplitView Subview Frames %@", self.autosaveName!)
        let subViewFrames = UserDefaults.standard.array(forKey: key)
        guard subViewFrames != nil else { return }

        for (i, frame) in (subViewFrames?.enumerated())! {

            if let frameString = frame as? String {

                let components = frameString.components(separatedBy: ", ")
                guard components.count >= 4 else { return }

                var position: CGFloat = 0.0

                // Manage the 'hidden state' per view
                let hidden = NSString(string:components[4].lowercased()).boolValue
                let subView = self.subviews[i]
                subView.isHidden = hidden

                // Set height (horizontal) or width (vertical)
                if self.isVertical {
                    if let n = NumberFormatter().number(from: components[2]) {
                        position = CGFloat(n)
                    }
                } else {
                    if let n = NumberFormatter().number(from: components[3]) {
                        position = CGFloat(n)
                    }
                }

                setPosition(position, ofDividerAt: i)
            }
        }
    }
}
caxix
  • 1,085
  • 1
  • 13
  • 25
-1

I am using an NSSplitViewController and I was only seeing the collapsed status of items not being restored (dimensions were correct). I could see that the information was being saved correctly.

Based on a variety of other answers here I created the following extension:

extension NSSplitViewController
{
    public func ensureRestoreCollapsed()
    {
        guard let autosaveName = splitView.autosaveName else { return }

        let framesKey = "NSSplitView Subview Frames \(autosaveName)"
        guard let subViewFrames = UserDefaults.standard.array(forKey: framesKey) else { return }

        for (i, frame) in subViewFrames.enumerated() {
            guard let hidden = (frame as? String)?.components(separatedBy: ", ")[safe: 4] else {
                return }
            
            splitViewItems[safe: i]?.isCollapsed = hidden.boolValue
        }
    }
}
Giles
  • 1,428
  • 11
  • 21