KVO: not receiving notifications on NSTableView's -selectedRowIndexes? - macos

I'm trying to have a custom subclass of NSTableView observe the value of its own -selectedRowIndexes property, and I'm having trouble figuring out how to receive the notifications properly. My subclass looks like this (using ARC):
#implementation MyTableView
- (id)initWithCoder:(NSCoder *)aDecoder {
if ((self = [super initWithCoder:aDecoder])) {
[self addObserver:self forKeyPath:#"selectedRowIndexes" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:NULL];
}
return self;
}
- (void)dealloc {
[self removeObserver:self forKeyPath:#"selectedRowIndexes"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
NSLog(#"change: %#", change);
}
#end
However, I never see -observeValueForKeyPath:... get called. Am I missing something?
I'm also open to a better solution - the reason I want to do KVO rather than relying on the delegate's -tableViewSelectionDidChange: method is that I'd like both the previous and current values for selectedRowIndexes, rather than just being able to get the current selection. If there's a way to do that without KVO on this property, I'm all ears.

If you're not seeing KVO notifications, I would open a radar at bugreport.apple.com. The reason is likely that they're not fully KVO compliant. I haven't tested, but I wouldn't be shocked.
As to how to do this without KVO, that's fairly straightforward. Use tableView:willSelectRowAtIndexPath: tableView:shouldSelectRow:. Check the current value, and the value to be added. Return YES.

I had the same problem, and I found the solution :
Bind the NSTableView view Selection Indexes to the array controller, key selectionIndexes

Related

NSTableView Binding and Observers

I am trying to add a new row to an NSTableview using bindings. According to a past post I was instructed to addObject on the array controller and the KVO should handle the notification to the view. However,
I get the following error:
An -observeValueForKeyPath:ofObject:change:context: message was received but not handled.
Key path: accountArray
Observed object: <AppDelegate: 0x10011e3b0>
Change: {
indexes = "<NSIndexSet: 0x102915ba0>[number of indexes: 1 (in 1 ranges), indexes: (2)]";
kind = 2;
}
I have added this observer in my App Delegate:
[self addObserver:self forKeyPath:#"accountArray" options:0 context:#"myContext"];
I have also tried to implement the observerValueforKeyPath but when I debug my code never gets to this point.
What am I doing wrong?
You must implement that method:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context

NSOutlineView outlineViewSelectionDidChange

my NSOutlineView outlineViewSelectionDidChange method will not be called.
I set set the NSOutlineViews delegate to the class where the other methods such as
- (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item
exist. But outlineViewSelectionDidChange will not be called on selecting an item.
Does anybody has an idea?
This notification is a bit odd, in that it is not automatically forwarded to delegates. Try adding an explicit registration to your initialization code, like this example:
- (void)windowControllerDidLoadNib:(NSWindowController *)aController;
{
[super windowControllerDidLoadNib:aController];
NSNotificationCenter * center = [NSNotificationCenter defaultCenter];
[center addObserver:self
selector:#selector(outlineViewSelectionDidChange:)
name:#"NSOutlineViewSelectionDidChangeNotification"
object:outlineView];
}
Okay,
meanwhile i figured out that the "NSOutlineViewSelectionDidChangeNotification" will be thrown only within the notification object. So i had to subclass my NSOutlineView to catch the notification and pass it to the object where i need it.
Your own view needs to conform to the NSOutlineViewDelegate protocol like so..
#interface MyOutlineViewController : NSView <NSOutlineViewDataSource,NSOutlineViewDelegate> {
IBOutlet NSOutlineView *myoutlineview;
}
#end
you will have this methods in your implementation
-(NSInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item;
-(BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item;
-(id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item;
-(id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item;
where you setup your outlineview.
When loading this view -(void)viewDidLoad gets called and your predefined nib/xib file or your manual call will set your datasource to fill it depending on your logic.
Now in your -(void)viewDidLoad your myoutlineview needs to set its own delegate with
[myoutlineview setDelegate:self];
so your own View may know where to call its notification methods triggerd from selections and so on. So you can place your notification logic inside the same View class conforming to this protocol.
-(void)outlineViewSelectionDidChange:(NSNotification *)notification {
NSLog(#"selection did change");
}

How to extend a Cocoa protocol in a category to avoid "not found in protocol(s)" warning?

This seems like a simple thing, but my brain doesn't seem to be working today, and my searches haven't turned up a helpful answer.
I have lots of code that extends Cocoa classes via categories (it's open source, too). Some methods want to call the delegate; the old code used informal protocols to do this, but now when building targeting 10.6, I get the warning:
warning: '-outlineView:menuForTableColumn:byItem:' not found in protocol(s)
As an example, here's a category:
#interface NSOutlineView (DSOutlineViewCategories)
- (NSMenu *)menuForEvent:(NSEvent *)event;
#end
Which used an informal protocol to declare a delegate method:
#interface NSObject (DSTableViewDelegate)
- (NSMenu *)outlineView:(NSOutlineView *)olv menuForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item;
#end
And the implementation calls that on the delegate:
#implementation NSOutlineView (DSOutlineViewCategories)
- (NSMenu *)menuForEvent:(NSEvent *)event
{
NSPoint point = [self convertPoint:[event locationInWindow] fromView:nil];
NSInteger column = [self columnAtPoint:point];
NSInteger row = [self rowAtPoint:point];
id item = [self itemAtRow:row];
if (column >= 0 && item && [[self delegate] respondsToSelector:#selector(outlineView:menuForTableColumn:byItem:)])
return [[self delegate] outlineView:self menuForTableColumn:[[self tableColumns] objectAtIndex:column] byItem:item];
else
return [super menuForEvent:event];
}
#end
How can I update this code for 10.6 (and beyond), to avoid the "not found in protocol(s)" warning?
I think this is because the NSOutlineView delegate is now typed as id <NSOutlineViewDelegate> rather than a plain id as it was in the 10.5 SDK. The category is declared on NSObject, but the compiler doesn't see the delegate object as inheriting from NSObject, so it doesn't recognize that it would respond to the message. Before, since the delegate was a plain id, it wouldn't complain about any message sent to it, as long as it could find the declaration somewhere.
The quick and dirty fix would be to just add a cast, making the code [(id)[self delegate] outlineView:self menuForTableColumn:[[self tableColumns] objectAtIndex:column] byItem:item];
To be a little more formal, you could declare your own formal delegate protocol which inherits from NSOutlineViewDelegate, which would look like
#protocol DSOutlineViewDelegate <NSOutlineViewDelegate>
#optional
- (NSMenu *)outlineView:(NSOutlineView *)olv menuForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item;
#end
Then, in the code that calls it, you would want to be calling the method on an object with type id <DSOutlineViewDelegate>. You can do this by declaring a new method that does the casting for you, like:
- (id <DSOutlineViewDelegate>)ds_delegate
{
return (id <DSOutlineViewDelegate>)[self delegate];
}
Then, in the actual code, you'd call:
[[self ds_delegate] outlineView:self menuForTableColumn:[[self tableColumns] objectAtIndex:column] byItem:item];
and the compiler should be OK with that. Since the method is declared as optional in the protocol, you still want to check at runtime whether the delegate actually responds to the selector.

Key Value Observing and NSButton state

I'm trying to observe checkbox status and make appropriate changes in the app when checkbox status changes. In a window manager that manages the window with checkbox I have following observer setup:
- (void)awakeFromNib
{
[myCheckBox addObserver:self
forKeyPath:#"state"
options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld)
context:NULL];
}
- (void)dealloc
{
[myCheckBox removeObserver:self forKeyPath:#"state"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
NSLog(#"KeyPath: %#", keyPath);
NSLog(#"ofObject: %#", object);
NSLog(#"change: %#", change);
}
I also have wired up myCheckBox to file owner (which is window controller) to appropriate checkbox in the window. However when I run my app observeValueForKeyPath:ofObject:change:context: method is never called.
What am I doing wrong?
In -awakeFromNib check that myCheckbox is not nil. If it's nil then it's not connected properly in IB.
Edit: NSButton.state has the same value as NSButton.cell!.state, but it isn't Key-Value Observable. To be able to observe the value, you'll need to use the \.cell!.state key path.
Unless documented to be Key Value Observing compliant, you should not expect the accessors of a given class to implement KVO support.
Buttons do implement key value binding, so instead of observing the state property you might bind one of your boolean attributes to the button's value binding.

How to create a binding for NSApp.dockTile's

In IB it is easy to bind a label or text field to some controller's keyPath.
The NSDockTile (available via [[NSApp dockTile] setBadgeLabel:#"123"]) doesn't appear in IB, and I cannot figure out how to programmatically bind its "badgeLabel" property like you might bind a label/textfield/table column.
Any ideas?
NSDockTile doesn't have any bindings, so your controller will have to update the dock tile manually. You could do this using KVO which would have the same effect as binding it.
Create a context as a global:
static void* MyContext=(void*)#"MyContext";
Then, in your init method:
[objectYouWantToWatch addObserver:self forKeyPath:#"dockTileNumber" options:0 context:MyContext];
You then have to implement this method to be notified of changes to the key path:
- (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if (context == MyContext) {
[[NSApp dockTile] setBadgeLabel:[object valueForKeyPath:keyPath]];
}
else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
Make sure you remove the observer when the controller object goes away.
If NSDockTile does support bindings, you can use the method bind:toObject:withKeyPath:options: to set up bindings on the badgeLabel property. Check the documentation for details on which arguments to use. If it doesn't work, you could either implement key value observing in your controller class and update the label each time the value changes, or even override NSDockTile to create a bindings compatible subclass.
I've tried lots of variations of bind:toObject:withKeyPath:options: on NSDockTile, on a controller, on the data source. I can't figure out a combination that works. Alternately, is there a way of having a BatchController object that can be bound to the data source, and it then updates the badge? How do I take an NSObject and make it bindable?

Resources