The Background
I've built a source list (similar to iTunes et al.) in my Cocoa app.
I've got an NSOutlineView, with Value
column bound to arrangedObjects.name
key path of an NSTreeController.
The NSTreeController accesses
JGSourceListNode entities in a Core
Data store.
I have three subclasses of
JGSourceListNode - JGProjectNode,
JGGroupNode and JGFolderNode.
I have selectedIndexPaths on NSTreeController bound to an NSArray called selectedIndexPaths in my App Delegate.
On startup, I search for group nodes and if they're not found in the core data store I create them:
if ([allGroupNodes count] == 0) {
JGGroupNode *rootTrainingNode = [JGGroupNode insertInManagedObjectContext:context];
[rootTrainingNode setNodeName:#"TRAIN"];
JGProjectNode *childUntrainedNode = [JGProjectNode insertInManagedObjectContext:context];
[childUntrainedNode setParent:rootTrainingNode];
[childUntrainedNode setNodeName:#"Untrained"];
JGGroupNode *rootBrowsingNode = [JGGroupNode insertInManagedObjectContext:context];
[rootBrowsingNode setNodeName:#"BROWSE"];
JGFolderNode *childFolder = [JGFolderNode insertInManagedObjectContext:context];
[childFolder setNodeName:#"Folder"];
[childFolder setParent:rootBrowsingNode];
[context save:nil];
}
What I Want
When I start the app, I want both top level groups to be expanded and "Untrained" to be highlighted as shown:
My Window http://synapticmishap.co.uk/Window.jpeg
The Problem
I put the following code in the applicationDidFinishLaunching: method of the app delegate:
[sourceListOutlineView expandItem:[sourceListOutlineView itemAtRow:0]];
[sourceListOutlineView expandItem:[sourceListOutlineView itemAtRow:2]];
NSIndexPath *rootIndexPath = [NSIndexPath indexPathWithIndex:0];
NSIndexPath *childIndexPath = [rootIndexPath indexPathByAddingIndex:0];
[self setSelectedIndexPaths:[NSArray arrayWithObject:childIndexPath]];
but the outline view seems to not have been prepared yet, so this code does nothing.
Ideally, eventually I want to save the last selection the user had made and restore this on a restart.
The Question
I'm sure it's possible using some crazy KVO to observe when the NSTreeController or NSOutlineView gets populated then expand the items and change the selection, but that feels clumsy and too much like a work around.
How would I do this elegantly?
Elegantly? This isn't elegant but it's how I'm doing it. I just do it manually. At app quit I write this value to user defaults:
lastSelectedRow = [outlineView selectedRow]
Then at app launch I run this in app did finish launching:
[self performSelector:#selector(selectLastNoteOrCreateDefaultNote) withObject:nil afterDelay:1];
Notice I just use a delay because I noticed the same as you that "the outline view seems to not have been prepared yet". Then in that selector I use this.
[outlineView selectRowIndexes:[NSIndexSet indexSetWithIndex:lastSelectedRow] byExtendingSelection:NO];
It works but better (more elegant) solutions are welcome from me too.
Related
So I guess this is going to be closed for being too subjective and too opinion based but if anyone can help me I would appreciate it.
I got a question. If I have a few controllers that all have almost the same thing For example they have the same background, have a menu going around the edge but the actual content is different. I had a couple of ideas. 1) Just have one view controller and just kill the objects for that current view if the user chooses a different option on the menu and spawn the new objects for that menu. My issue with this way was that I could't find a way to use the auto layout with this.
Second way would to be have a function in a .swift file that I can call and it creates an image view and sets up the menu an everything like that. I have the opposite issue here though, now the auto layout won't work.
App devs must have a way of doing this, I'm just probably thinking of this completely the wrong way.
Is there a better way to be doing this - I am sure there is? I would appreciate it if someone could point me in the correct direction.
Thanks
EDIT:
I should make it clear that the language I am using is swift.
You can create custom container view controller and swap the view controllers for the part that change according to the user selection.
--Adding Example--
e.g iPad's Settings app. The left side is a table view and right side is detail view which changes on user selection. So Tableview can be wrapped in a view controller let's say ListViewController. This will not change. The right side will be DetailViewController which would be swapped according to user selection. Your ContainerViewController will have 2 view controllers at all times.
Here is how to add view controllers as child and set their views in objective-c.
- (void) setupContentViewControllerWith: (DetailViewController*) detailViewController andListViewController:(ListViewController*)listViewController {
[self addChildViewController:listViewController];
[self addChildViewController:detailViewController];
listViewController.view.frame = CGRectMake(kListView_X, kListView_Y, kListView_Width, kListView_Height);
detailViewController.view.frame = CGRectMake(kListView_Width, kDetailView_Y, self.view.bounds.size.width, self.view.bounds.size.height-kDetailView_Y);
[self.scrollContainer addSubview:listViewController.view];
[self.scrollContainer addSubview:detailViewController.view];
[self.scrollContainer setContentSize:CGSizeMake(kListView_Width+self.view.bounds.size.width, self.view.bounds.size.height)];
}
When user selects new item from the list, you can swap DetailViewControllers as below
- (void) replaceEpisodeControllerWith:(DetailViewController *)detailViewController {
detailViewController.view.frame = CGRectMake(kListView_Width, kDetailView_Y, self.view.bounds.size.width, self.view.bounds.size.height-kDetailView_Y);
[UIView transitionFromView:currentDetailViewController.view
toView:detailViewController.view
duration:0.0
options:UIViewAnimationOptionTransitionNone
completion:^(BOOL finished)
{
[currentDetailViewController.view removeFromSuperview];
[currentDetailViewController removeFromParentViewController];
[currentDetailViewController release];
currentDetailViewController = detailViewController;
}];
}
I don't have swift version of this.
I have a UITableView that stores the selected row in User Defaults. The tableView is part of a menu structure that may be reloaded during the lifetime of the application, hence I want the persistence between loads. In viewDidLoad, this UserDefault is checked for existence, and I call
NSIndexPath *path = [NSIndexPath indexPathForRow:row inSection:0];
[self.tableView selectRowAtIndexPath:path animated:NO scrollPosition:(UITableViewScrollPositionMiddle)];
This works fine, as expected. However, it doesn't actually select the row, it just highlight's it. If I subsequently call
[self tableView:self.tableView didSelectRowAtIndexPath:path];
I get a crash - "unrecognised selector sent to instance". Why?
[self tableView:self.tableView didDeselectRowAtIndexPath:path] will call YOUR implementation of the deselection method (which is defined in UITableViewDelegate).
You get a crash since you didn't implement it in your delegate.
You should call:
[self.tableView deselectRowAtIndexPath:path animated:NO];
The UITableView works via two delegate protocols, UITableViewDelegate and UITableViewDataSource. The class that defines your UITableView should implement these protocols and you should set the delegate and datasource of the UITableView to "self". You should not call the protocol methods directly which is most likely the reason for your crash.
In order to select a particular row based on your data model (User Default), you will need to set the UITableViewCell selected property to "YES" in the tableView:cellForRowAtIndexPath: for the row that is being rendered.
I recommend going through this tutorial to help you understand UITableView's better.
http://www.iosdevnotes.com/2011/10/uitableview-tutorial/
I have a NSArrayController and a NSTableView. They show tracks from iTunes. I can sort the list by clicking in the header.
Is there a way to set up a default sort descriptor for the table view so it sorts for albums every time the user launches the app?
I tried to set the sortDescriptor on the array controller and the table view but that changes nothing.
Thank you
Edit: The answer is right. But it needs a NSArray:
- (NSArray *)mainSortDescriptor {
return [NSArray arrayWithObjects:
[NSSortDescriptor sortDescriptorWithKey:#"album" ascending:YES],
[NSSortDescriptor sortDescriptorWithKey:#"trackNumber" ascending:YES],
nil];
}
If you want to bind the array controller's sort descriptor, you have to bind it to something. You can put this in your application delegate, for example:
- (NSArray *)tracksSortDescriptors {
return [NSArray arrayWithObject:
[NSSortDescriptor sortDescriptorWithKey:#"albumName"
ascending:YES]];
}
Then you can set up the binding in IB as
Bind to: MyAppDelegate
Model Key Path: tracksSortDescriptors
EDITED. I forgot, when translating this from PyObjC, that I was returning a list. Oops.
I tried this, didn't quite work - resorted on each app start, but not while the app was running.
Eventually, I noticed that in my NSArrayController object, the following box was unticked (argh!):
"Auto rearrange content"
...so, FYI to anyone who has the same problem: make sure that box is ON :)
in my cocoa application, I need a custom NSCell for an NSTableView. This NSCell subclass contains a custom NSButtonCell for handling a click (and two or three NSTextFieldCells for textual contents). You'll find a simplified example of my code below.
#implementation TheCustomCell
- (void)drawWithFrame:(NSRect)cellFrame inView:(NSView *)controlView {
// various NSTextFieldCells
NSTextFieldCell *titleCell = [[NSTextFieldCell alloc] init];
....
// my custom NSButtonCell
MyButtonCell *warningCell = [[MyButtonCell alloc] init];
[warningCell setTarget:self];
[warningCell setAction:#selector(testButton:)];
[warningCell drawWithFrame:buttonRect inView:controlView];
}
The problem I'm stuck with is: what is the best/right way to get that Button (more precisely: the NSButtonCell) inside this NSCell to work properly? "work" means: trigger the assigned action message and show the alternate image when clicked. Out of the box, the button doesn't do anything when clicked.
Information / readings on this topic is hard to find. The only posts I found on the net pointed me to implementing
- (BOOL)trackMouse:(NSEvent *)theEvent inRect:(NSRect)cellFrame ofView:(NSView *)controlView untilMouseUp:(BOOL)untilMouseUp;
Is this the correct way to do it??? Implement trackMouse: in my containing NSCell? And then forward the event to the NSButtonCell? I would have expected the NSButtonCell itself to know what to do when it's being clicked (and I saw the trackMouse: methods more in cunjunction with really tracking mouse movements - not as a training wheel for 'standard' click behaviour). But it seems like it doesn't do this when included in a cell itself...
It seems I haven't grasped the big picture on custom cells, yet ;-)
I'd be glad if someone could answer this (or point me to some tutorial or the like) out of his own experience - and tell me if I'm on the right track.
Thanks in advance,
Tobi
The minimal requirements are:
After left mouse down on the button, it must appear pressed whenever the mouse is over it.
If the mouse then releases over the button, your cell must send the appropriate action message.
To make the button look pressed, you need to update the button cell's highlighted property as appropriate. Changing the state alone will not accomplish this, but what you want is for the button to be highlighted if, and only if, its states is NSOnState.
To send the action message, you need to be aware of when the mouse is released, and then use -[NSApplication sendAction:to:from:] to send the message.
In order to be in position to send these messages, you will need to hook into the event tracking methods provided by NSCell. Notice that all those tracking methods, except the final, -stopTracking:... method, return a Boolean to answer the question, "Do you want to keep receiving tracking messages?"
The final twist is that, in order to be sent any tracking messages at all, you need to implement -hitTestForEvent:inRect:ofView: and return an appropriate bitmask of NSCellHit... values. Specifically, if the value returned doesn't have the NSCellHitTrackableArea value in it, you won't get any tracking messages!
So, at a high level, your implementation will look something like:
- (NSUInteger)hitTestForEvent:(NSEvent *)event
inRect:(NSRect)cellFrame
ofView:(NSView *)controlView {
NSUInteger hitType = [super hitTestForEvent:event inRect:cellFrame ofView:controlView];
NSPoint location = [event locationInWindow];
location = [controlView convertPointFromBase:location];
// get the button cell's |buttonRect|, then
if (NSMouseInRect(location, buttonRect, [controlView isFlipped])) {
// We are only sent tracking messages for trackable areas.
hitType |= NSCellHitTrackableArea;
}
return hitType;
}
+ (BOOL)prefersTrackingUntilMouseUp {
// you want a single, long tracking "session" from mouse down till up
return YES;
}
- (BOOL)startTrackingAt:(NSPoint)startPoint inView:(NSView *)controlView {
// use NSMouseInRect and [controlView isFlipped] to test whether |startPoint| is on the button
// if so, highlight the button
return YES; // keep tracking
}
- (BOOL)continueTracking:(NSPoint)lastPoint at:(NSPoint)currentPoint inView:(NSView *)controlView {
// if |currentPoint| is in the button, highlight it
// otherwise, unhighlight it
return YES; // keep on tracking
}
- (void)stopTracking:(NSPoint)lastPoint at:(NSPoint)stopPoint inView:(NSView *)controlView mouseIsUp:(BOOL)flag {
// if |flag| and mouse in button's rect, then
[[NSApplication sharedApplication] sendAction:self.action to:self.target from:controlView];
// and, finally,
[buttonCell setHighlighted:NO];
}
The point of NSCell subclasses is to separate responsibility for rendering and handling common UI elements (the controls) from the visual- and event-hierarchy
responsibilities of the NSView classes. This pairing permits each one to provide greater specialization and variability without burdening the other. Look at the large number of NSButton instances one can create in Cocoa. Imagine the number of NSButton sub-classes that would exist if this split in functionality were absent!
Using design pattern language to describe the roles: an NSControl acts as a façade, hiding details of its composition from its clients and passing events and rendering messages to its NSCell instance which acts as a delegate.
Because your NSCell subclass includes other NSCell subclass instances within its composition, they no longer directly receive these event messages from the NSControl instance which is in the view hierarchy. Thus, in order for these cell instances to receive event messages from the event responder chain (of the view hierarchy), your cell instance needs to pass along those relevant events. You are recreating the work of the NSView hierarchy.
This isn't necessarily a bad thing. By replicating the behavior of NSControl (and its NSView superclass) but in an NSCell form, you can filter the events passed on to your sub-cells by location, event type, or other criteria. The drawback is replicating the work of NSView/NSControl in building the filtering & management mechanism.
So in designing your interface, you need to consider whether the NSButtonCell (and NSTextFieldCells) are better off in NSControls in the normal view hierarchy, or as sub-cells in your NSCell subclass. It's better to leverage the functionality which already exists for you in a codebase than to re-invent it (and continue maintaining it later) unnecessarily.
I'm using an NSArrayController, NSMutableArray and NSTableView to show a list of my own custom objects (although this question probably applies if you're just showing a list of vanilla NSString objects too).
At various points in time, I need to clear out my array and refresh the data from my data source. However, just calling removeAllObjects on my NSMutableArray object does not trigger the KVO updates, so the list on screen remains unchanged.
NSArrayController has no removeAllObjects method available, which seems really weird. (It does have addObject, which I use to add the objects, ensuring the KVO is triggered and the UI is updated.)
The cleanest way I've managed to cause this happen correctly is:
[self willChangeValueForKey:#"myArray"];
[myArray removeAllObjects];
[self didChangeValueForKey:#"myArray"];
...so I'm kind of having to do the KVO notification manually myself (this is in my test app class, that contains the myArray property, which is NSMutableArray, as mentioned.)
This seems wrong - is there a better way? From my googling it seems a few people are confused by the lack of removeAllObjects in NSArrayController, but haven't seen any better solutions.
I have seen this solution:
[self removeObjectsAtArrangedObjectIndexes:
[NSIndexSet indexSetWithIndexesInRange:
NSMakeRange(0, [[self arrangedObjects] count])]];
but this looks even more unpleasant to me. At least my solution is at least marginally self-documenting.
Did Apple not notice that sometimes people might want to empty a list control being managed via an NSArrayController object? This seems kind of obvious, so I think I must be missing something...
Aside: of course, if I add new items to the array (via NSArrayController), then this triggers a KVO update with the NSArrayController/NSTableView, but:
Sometimes I don't put any items in the list, because there are none. So you just see the old items.
This is a bit yucky anyway.
You don't remove items from a table view. It doesn't have any items—it just displays another object's items.
If you bound the array controller's content array binding to an array property of some other object, then you should be working with that property of that object. Use [[object mutableArrayValueForKey:#"property"] removeAllObjects].
If, on the other hand, you haven't bound the array controller's content array binding, then you need to interact with its content directly. Use [[arrayController mutableArrayValueForKey:#"content"] removeAllObjects]. (You could also work with arrangedObjects instead of content. If one doesn't work, try the other—I've only ever done things the first way, binding the array controller to something else.)
Had this problem as well and solved it this way:
NSArrayController* persons = /* your array controller */;
[[persons content] removeAllObjects];
Swift
#IBOutlet var acLogs: NSArrayController!
acLogs.removeObjects(acLogs.content as! [AnyObject])
worked for me.
Solution in Swift:
if let ac = arrayController
{
let range:NSRange = NSMakeRange(0, ac.arrangedObjects.count);
let indexSet:NSIndexSet = NSIndexSet(indexesInRange: range);
ac.removeObjectsAtArrangedObjectIndexes(indexSet);
}
Just an update that works in Swift 4:
let range = 0 ..< (self.arrayController.arrangedObjects as AnyObject).count
self.arrayController.remove(atArrangedObjectIndexes: IndexSet(integersIn: range))