Jordan Smith
Jordan Smith

Reputation: 10378

NSSplitVIew - auto saving divider positions doesn't work with auto layout enabled

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)?

Upvotes: 20

Views: 4401

Answers (8)

Giles
Giles

Reputation: 1658

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
        }
    }
}

Upvotes: -1

csch
csch

Reputation: 1455

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!"
    }
}

        

Upvotes: 36

user187676
user187676

Reputation:

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"
}

Upvotes: 11

caxix
caxix

Reputation: 1102

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)
            }
        }
    }
}

Upvotes: 1

Joris Borst Pauwels
Joris Borst Pauwels

Reputation: 61

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 ) ];
            }
        }
    }
}

Upvotes: 4

ElmerCat
ElmerCat

Reputation: 3155

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];
}

Upvotes: 2

marcprux
marcprux

Reputation: 10355

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.

Upvotes: 14

silvansky
silvansky

Reputation: 2427

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).

Upvotes: 1

Related Questions