Autosave Expanded Items of NSOutlineView doesn't work
Asked Answered
V

8

9

I am trying to use the "Autosave Expanded Items" feature. When I expand a group with its children and restart the application all children are collapsed again and I don't know why they won't stay expanded. I'm using core data to store my source list items.

This is what I have done/set so far:

  • Checked "Autosave Expanded Items" in NSOutlineView (Source List)
  • Set a name for "Autosave"
  • dataSource and delegate outlets assigned to my controller

This is my implementation for outlineView:persistentObjectForItem and outlineView:itemForPersistentObject.

- (id)outlineView:(NSOutlineView *)anOutlineView itemForPersistentObject:(id)object
{
    NSURL *objectURI = [[NSURL alloc] initWithString:(NSString *)object];  
    NSManagedObjectID *mObjectID = [_persistentStoreCoordinator managedObjectIDForURIRepresentation:objectURI]; 
    NSManagedObject *item = [_managedObjectContext existingObjectWithID:mObjectID error:nil];
    return item;  
}

- (id)outlineView:(NSOutlineView *)anOutlineView persistentObjectForItem:(id)item
{
    NSManagedObject *object = [item representedObject];
    NSManagedObjectID *objectID = [object objectID];
    return [[objectID URIRepresentation] absoluteString];
}

Any ideas? Thanks.

EDIT: I have a clue! The problem is maybe that the tree controller has not prepared its content on time. The methods applicationDidFinishLaunching, outlineView:persistentObjectForItem etc. are being be executed before the data has loaded or rather the NSOutlineView hasn't finished initializing yet. Any ideas how to solve this?

Voracity answered 11/9, 2014 at 14:3 Comment(1)
Did you find a solution? I have a similar problem, although I don't use CoreData and use bindings. Indeed the method outlineView:itemForPersistentObject: is called before the app finished launching.Thanatos
M
4

I've had the problem that my implementation of -outlineView:itemForPersistentObject: was not called at all. It turns out that this method is called when either "autosaveExpandedItems" or "autosaveName" is set. My solution was to set both properties in Code and NOT in InterfaceBuilder. When i set the properties after the delegate is assigned, the method gets called.

Moffat answered 6/3, 2016 at 15:8 Comment(0)
C
2

I got this to work - you need to return the corresponding tree node instead of "just" its represented object.

In itemForPersistentObject:, instead of return item; you need return [self itemForObject:item inNodes:[_treeController.arrangedObjects childNodes]];

with

- (id)itemForObject:(id)object inNodes:(NSArray *)nodes {
    for (NSTreeNode *node in nodes) {
        if ([node representedObject] == object)
            return node;

        id item = [self itemForObject:object inNodes:node.childNodes];
        if (item)
            return item;
    }

    return nil;
}

where _treeController is the NSTreeController instance that you use to populate the outline view.

Chromatography answered 17/7, 2015 at 9:47 Comment(0)
E
2

Expanding on Karsten's solution:

The method -outlineView:itemForPersistentObject: gets called after doing what Karsten suggests, but ONLY if you also set the datasource before setting the delegate.

So if Karsten's answer doesn't seem to work, check where your datasource is set and adjust accordingly.

(wanted to write this as a comment but I'm not allowed due to my newbie status ...)

Edisonedit answered 15/9, 2017 at 6:53 Comment(0)
R
2

Wow! 6 years later and this is still causing headaches.

I couldn't get this working initially, even with Karsten's helpful solution re setting autoSaveName & autosaveExpandedItems in code; itemForPersistentObject was still being called before the outlineView was populated. The solution for me, whilst not very elegant, was to set a delay of .5 seconds before setting autosaveExpandedItems & autoSaveName. The half second delay in my app is not noticeable. I used Vomi's code as well. Delegate and dataSource are set in IB bindings. Here's full solution:

override func viewDidLoad() {
    super.viewDidLoad()

    let _ = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { (timer) in
        self.keywordsOutlineView.autosaveExpandedItems = true
        self.keywordsOutlineView.autosaveName = "KeywordsOutlineView"
        timer.invalidate()
    }

}

func outlineView(_ outlineView: NSOutlineView, persistentObjectForItem item: Any?) -> Any? {
    
    if let node = item as? NSTreeNode {
        if let object = node.representedObject as? FTKeyword {
            return object.objectID.uriRepresentation().absoluteString
        }
    }
    return nil
}

// This method should return a NSTreeNode object
func outlineView(_ outlineView: NSOutlineView, itemForPersistentObject object: Any) -> Any? {
    
    if outlineView == keywordsOutlineView {
        
        guard let uriAsString = object as? String,
            let uri = URL(string: uriAsString) else { return nil }
        
            if let psc = self.managedObjectContext.persistentStoreCoordinator,
                let moID = psc.managedObjectID(forURIRepresentation: uri),
                let group = self.managedObjectContext.object(with: moID) as? FTKeyword,
                let nodes = self.keywordsTreeController.arrangedObjects.children {
                
                return self.findNode(for: group, in: nodes)
            }
            return nil
        

    }
    return nil
}

/// Utility method to find the corresponding NSTreeNode for a given represented object
private func findNode(for object: NSManagedObject, in nodes: [NSTreeNode]) -> NSTreeNode? {
    
    for treeNode in nodes {
        if (treeNode.representedObject as? NSManagedObject) === object {
            return treeNode
        }
    }
    return nil
}
Rori answered 5/8, 2021 at 14:18 Comment(1)
Hello. I have the same problem. When the autosavename property is set in IB, itemForPersistentObject: is called before the tree controller is populated, which is dumb (I'm not sure why appkit doesn't check for that). I set the autosaveName property to trigger the call when the content property of the tree controller changes (using an observer). This works.Shay
C
1

Swift 5 answer

Karsten is right, itemForPersistentObject must return a NSTreeNode.

Here is a Swift 5 version of the solution:

// This method should return a NSTreeNode object
func outlineView(_ outlineView: NSOutlineView, itemForPersistentObject object: Any) -> Any? {
    guard let uriAsString = object as? String,
    let uri = URL(string: uriAsString) else { return nil }

    if let psc = self.managedObjectContext.persistentStoreCoordinator,
        let moID = psc.managedObjectID(forURIRepresentation: uri),
        let group = self.managedObjectContext.object(with: moID) as? MyGroupEntity,
        let nodes = self.expensesTreeController.arrangedObjects.children {
        return self.findNode(for: group, in: nodes)
    }
    return nil
}

/// Utility method to find the corresponding NSTreeNode for a given represented object
private func findNode(for object: NSManagedObject, in nodes: [NSTreeNode]) -> NSTreeNode? {
    for treeNode in nodes {
        if (treeNode.representedObject as? NSManagedObject) === object {
            return treeNode
        }
    }
    return nil
}
Countermove answered 11/11, 2019 at 10:58 Comment(3)
how do you get this working if you're using a tree controller? I have to set the ViewController as the data source but then it complains about all kinds of other missing functions, such as numberOfChildrenOfItem and so on, which presumably it would get from the treeControllerWohlert
@DuncanGroenewald, I do not use a tree controller, I encountered too many issues with it. I have more flexibility using datasource & delegate.Countermove
@Countermove Looks neat but I can't seem to find managedObjectContext and persistentStoreCoordinator anywhere. Found NSManagedObjectContext and NSPersistentStoreCoordinator though... Can you give me some hints?Shelbashelbi
E
1

I resolved this by setting autosaveName in Interface Builder AND autosaveExpandedItems in code in viewDidAppear:

override func viewDidAppear() {
    super.viewDidAppear()
    outlineView.autosaveExpandedItems = true
}

For my simple case when item is just [Int] these persistentObjectForItem: and itemForPersistentObject worked (I didn't read to return NSTreeNode):

func outlineView(_ outlineView: NSOutlineView, persistentObjectForItem item: Any?) -> Any? {
    return item
}

func outlineView(_ outlineView: NSOutlineView, itemForPersistentObject object: Any) -> Any? {
    return object
}
Eleonoreeleoptene answered 22/5, 2023 at 4:9 Comment(0)
V
0

I never got this working.

This is my current way of doing it:

First, I added an attribute "isExpanded" and saved for each node the status in the database.

enter image description here

Second, I expand the nodes when my treeController has prepared its content.

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{  
    [treeSectionController addObserver:self
                     forKeyPath:@"content"
                        options:0
                        context:nil]; 
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object     change:(NSDictionary *)change context:(void *)context
{
    if (object == treeSectionController) {
        NSArray *sectionArray = [[treeSectionController arrangedObjects]     childNodes];
        for (NSTreeNode *node in sectionArray) {
             if([[node representedObject] isExpandedValue]) {
                 [outlinePilesView expandItem:node];
             }
        }
        [treeSectionController removeObserver:self forKeyPath:@"content"];
    }
}
Voracity answered 11/3, 2015 at 10:3 Comment(1)
In my humble opinion, this is a very bad thing to do. Adding the isExpanded attribute to your model object just mixes the model and the user interface, and breaks the Model-View-Controller pattern. What will happen if tomorrow, you decide to change your outline view to a table or collection view ? This attribute becomes useless. What happen if you decide that your objects can be shown through 2 different outline views ? You will add an attribute ? Beside, if you save your model to the cloud, you will generate network traffic everytime your user expands/collapses an item ? Very bad...Aloise
T
0

Wait for NSTreeController’s arrangedObjects to be initialized.

var treeContentObserver: NSKeyValueObservation?

override func viewDidLoad() {
    super.viewDidLoad()
    // setup outlineview, treeController, bind content, etc.
    treeContentObserver = treeController.observe(\.arrangedObjects, options: [.initial]) { (_, _) in
        Task {
            await MainActor.run {
                self.outlineView.autosaveExpandedItems = true
                self.outlineView.autosaveName = "uniqueNameForThisOutlineView"
                if let observer = self.treeContentObserver {
                    observer.invalidate()
                    self.treeContentObserver = nil
                }
            }
        }
    }
}
Thespian answered 20/8 at 17:25 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.