Preference Pane & NSTableView bindings - cocoa

I am trying to create a preference pane which will reside within system preferences. All the bindings are done as usual (for a normal windowed application), but when the setter for the binding property is called (data is updated), the table data does not reset. Are preference panes capable of updating table data via bindings? I have also tried to use a table data source unsuccessfully.
To clarify, I have an NSMutableArray property in my prefPane's main class, an object representing the prefPane's main class, and an arrayController in IB which is bound to the table column. in the init method of the prefPane's main class, I set the value of the NSMutableArray, which is properly reflected in the pref pane, however, (just to test if bindings work), i have an NSTimer which resets the value of my NSMutable array when it finishes. A console message tells me that the value is properly reset, however, the changes are not reflected in the pref pane.
So in my current version i use the following code to set the properties to arbitrary values (simplified to try to get bindings to work at all). The property value is then reset by a timer 10 seconds later. Although the property is correctly updated (verified by console log), the pref pane does not reflect the changes in the tableview. Unfortunately, I cannot post screenshots of the bindings. I have an object in IB for the syncFrontEndPref class. I then have an arraycontroller bound to this object w/ a model key path of listArray. Then my table column is bound to the arraycontroller arranged objects. This loads properly with "test", "test1", "test2" in the pref pane (as populated from the init method). However, when repopulated from the timer, the changes are not reflected in the pref pane (although console log confirms listArray has indeed changed.
Here is the code:
#interface syncFrontEndPref : NSPreferencePane
{
NSMutableArray *listArray;
NSNumber *syncInt;
AuthenticateUser *newUser;
NSMutableArray *syncIntervalList;
IBOutlet NSTableView *theTableView;
}
#property (retain) NSMutableArray *listArray;
#property (retain) NSMutableArray *syncIntervalList;
- (void) mainViewDidLoad;
-(IBAction)syncIntervalValueChanged:(id)sender;
-(IBAction)tableViewSelected:(id)sender;
#implementation syncFrontEndPref
#synthesize listArray, syncIntervalList;
-(id) init{
//populate nsarray w/ list data to display
//[self setListArray: [NSMutableArray arrayWithArray:[[[NSDictionary dictionaryWithContentsOfFile:[GetFilePath pathForFile]] objectForKey:#"lists"] allObjects]]];
[self setListArray: [NSMutableArray arrayWithObjects: #"test", #"test1", #"test2", nil]];
//define values for drop-down sync interval selector
[self setSyncIntervalList:[NSMutableArray arrayWithObjects: #"1 minute", #"5 minutes", #"10 minutes", #"30 minutes", #"24 hours", nil]];
return self;
}
//code for the timer and selector method
- (void) mainViewDidLoad{
NSTimer *timer = [[NSTimer new] autorelease];
int syncTime = 10;
timer = [NSTimer scheduledTimerWithTimeInterval: syncTime target:self selector:#selector(targetMethod:) userInfo:nil repeats: YES];
}
-(void)targetMethod:(id)sender{
NSLog(#"running timer...");
[self setListArray: [NSMutableArray arrayWithObjects: #"0", #"1", #"2", nil]];
NSLog(#"%#", listArray);
}

I think you have two instances of your syncFrontEndPref object instantiated.
If you create a Preference Pane project from the template the File's Owner will be the NSPreferencePane. If you've added another entry for the syncFrontEndPref object, you will be creating a second copy of the object, and mainViewDidLoad won't be called in the second one. The timer won't be triggered for that copy of the object and the listArray won't be updated. Try adding a log statement to the init method. If you see that log statement run twice, you have two copies of the object.
If you do have two copies of the object, I'd suggest removing the copy you added to the xib in IB. Change the class of the File's Owner to your syncFrontEndPref class, and connect your bindings to that object.
Does this look something like your current xib file in IB?

Related

NSSplitViewController/NSSplitViewItem support in XIBs

Is there support for NSSplitViewController/NSSplitViewItem for XIBs? I see only NSSplitView
Can I just drag&drop NSViewController and subclass it as NSSplitViewController? How do I add NSSplitViewItem that it mostly works out of the box?
I can easily see support for them in storyboards.
The split view controller is not part of the object library for xib files. The easiest way to use split view controllers is to use storyboards.
If you are unwilling to use storyboards, your best option is to create a subclass of NSSplitViewController and select the checkbox to also create a xib file.
Add a split view to the split view controller xib file. Write code to load the xib file to set up the split view controller.
UPDATE
Look at the NSNib class reference for information on loading a xib file. The File's Owner of the xib file is your NSSplitViewController subclass. You may be able to use that information to set the split view controller. The worst case scenario is that you have to write code to load the split view from the xib file, set the split view controller's split view to the split view you loaded, and add the split view items to the split view controller. See the NSSplitViewController class reference for more information.
Yes it's possible. But it needs some wiring.
First add a custom subclass of NSSplitViewItem and expose viewController property as IBOutlet. Compiler will throw a warning so don't forget to mark property as dynamic.
#interface MySplitViewItem : NSSplitViewItem
#property IBOutlet NSViewController *viewController;
#end
#implementation MySplitViewItem
#dynamic viewController;
#end
In your XIB add 3 NSViewController objects. One of them change to custom class NSSplitViewController. It is important to note that one should NOT add NSSplitView. Wire NSViewControllers to it's views. Also add 2 objects and add custom class of MySplitViewItem which has exposed the viewController and wire it.
Last step. It is important to set property splitItems of NSSplitViewController before the views are loaded! Otherwise you are caught with NSAssert macro.
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
NSNib *nib = [[NSNib alloc] initWithNibNamed:#"Empty" bundle:nil];
NSMutableArray *test = [NSMutableArray new];
NSMutableArray *splitItems = [NSMutableArray new];
NSSplitViewController *controller;
[nib instantiateWithOwner:self topLevelObjects:&test];
for (id object in test) {
if ([object isKindOfClass:[NSSplitViewController class]]) {
controller = object;
}
if ([object isKindOfClass:[NSSplitViewItem class]]) {
[splitItems addObject:object];
}
}
[controller setValue:splitItems forKey:#"splitViewItems"];
[[self window] setContentViewController:controller];
}
Here is a proof that everything is wired correctly. Note that I did not touch delegate in XIB and it is wired. Magic, I know.
PS: XIB has to be set to prefer Coder + auto layout.
Why do I prefer XIB? Because we can create larger XIB which doesn't suffer from data isolation (Easily can do bindings across NSViewControllers).
I have also experimented to add splitViewItems in viewDidLoad or setView or awakeFromNib: in custom subclass of NSSplitViewController (with exposed NSSplitViewItem properties). If someone finds solution here it will be greatly appreciated.
Solution that requires code only:
- (NSSplitViewController *)profilesSVC
{
if (!_profilesSVC) {
NSSplitViewController *splitVC = [[NSSplitViewController alloc] init];
ProfilesViewController *profilesVC = [[ProfilesViewController alloc] initWithNibName:#"Profiles" bundle:nil];
NSSplitViewItem *leftItem = [NSSplitViewItem splitViewItemWithViewController:profilesVC];
[splitVC addSplitViewItem:leftItem];
ProfileViewController *profileVC = [[ProfileViewController alloc] initWithNibName:#"Profile" bundle:nil];
NSSplitViewItem *rightItem = [NSSplitViewItem splitViewItemWithViewController:profileVC];
[splitVC addSplitViewItem:rightItem];
_profilesSVC = splitVC;
}
return _profilesSVC;
}
I too wanted to add a splitView controller to my projet (macOS app) that doesn't use storyboards.
As it turned out, this was rather easy (in XCode 12.4).
As suggested, one has to to add NSViewController objects to the xib and wire each view property to the corresponding 'pane' (subview of the split view) in interface builder.
Then create a subclass of NSSplitViewController (no need to create a xib file).
Add a third NSViewController object to the xib and change its class to your subclass. Then wire both it's view and splitView properties to your splitView. It doesn't load any view if you just wire the splitView property.
Using a subclass of NSSplitViewController may not be required, but it's convenient as you may set the splitViewItems within viewDidLoad (below). Since this object is (automatically) the delegate of the splitView, you can also override delegate methods if you wish.
That object should have outlets leading to the NSViewController objects which you previously wired to the panes in IB.
I set two outlets named leftController and rightController.
My awakeFromNib method looks like this (sorry, I don't use swift):
- (void) viewDidLoad {
self.splitView.wantsLayer = YES; // I think this is required if you use a left sidebar with vibrancy (which I do below). Otherwise appkit complains and forces the use of CA layers anyway
NSSplitViewItem *left =[NSSplitViewItem sidebarWithViewController:leftController];
[self addSplitViewItem:left];
NSSplitViewItem *right =[NSSplitViewItem splitViewItemWithViewController:rightController];
right.minimumThickness = 420;
[self addSplitViewItem:right];
}
Voilà!
However, I get crashes if I set thick dividers in IB as appkit calls splitView:shouldHideDividerAtIndex too early, when there is apparently no divider yet. Worse, it may pass a negative divider index (!!). But you may override the method and act accordingly and I have no issue with thin dividers.

How to configure content for NSPopUpButton

I have a NSPopUpButton configured with bindings and coredata. Everything is working perfectly, however I would like to add a item that implements an action to "edit the list", like
Item 1
Item 2
Item 3
Item 4
------
Edit List..
Is this Possible to do with Bindings?
I think that the answer is NO, at least not completely. I thought I would provide the content to the button programatically and maintain bindings for the Selected Value , so this is what I came up with
- (void)updateSectorPopupItems
{
NSFetchRequest *request = [[NSFetchRequest alloc] initWithEntityName:#"Sector"];
NSSortDescriptor *sortPosition = [[NSSortDescriptor alloc] initWithKey:#"position" ascending:YES];
[request setSortDescriptors:#[sortPosition]];
NSError *anyError = nil;
NSArray *fetchObjects = [_gdcManagedObjectContext executeFetchRequest:request
error:&anyError];
if (fetchObjects == nil) {
DLog(#"Error:%#", [anyError localizedDescription]);
}
NSMutableArray *sectorNames = [NSMutableArray array];
for (NSManagedObject *sector in fetchObjects) {
[sectorNames addObject:[sector valueForKey:#"sectorCatagory"]];
}
[_sectorPopUpBotton addItemsWithTitles:sectorNames];
NSInteger items = [[_sectorPopUpBotton menu] numberOfItems];
if (![[_sectorPopUpBotton menu] itemWithTag:1] ) {
NSMenuItem *editList = [[NSMenuItem alloc] initWithTitle:#"Edit List..." action:#selector(showSectorWindow:) keyEquivalent:#""];
[editList setTarget:self];
[editList setTag:1];
[[_sectorPopUpBotton menu] insertItem:editList atIndex:items];
}
A couple of problems I'm having with this
1) When adding the Menu Item using
[_sectorPopUpBotton menu] insertItem:editList atIndex:items];
no matter what value is entered in atIndex, the item always appears at the top of the Menu list.
2) I just want the "Edit List..." menuitem to initiate the action, how do I prevent this from being selected as a value?
You might as well do that using an NSMenuDelegate method.
Actually in this way you can also keep the bindings for getting the NSPopUpButton content objects (in your case from the NSArrayController bound to the CoreData stack).
1) Set an object as delegate for the NSPopUpButton internal menu, you can do that in the Interface Builder by drilling down the NSPopUpButton to reveal its internal menu. Select it and then set its delegate in the Connections Inspector panel to the object you have designated to this task. As such delegate you might for example provide the same ViewController object which manages the view where the NSPopUpButton exists.
You'll then need to have the object provided as delegate adhere to the NSMenuDelegate informal protocol.
2) Implement the NSMenuDelegate method menuNeedsUpdate: there you'll add the NSmenuItem(s) (and eventually separators) you want to provide in addition to those already fetched by the NSPopButton's bindings.
An example code would be:
#pragma mark NSMenuDelegate
- (void)menuNeedsUpdate:(NSMenu *)menu {
if ([_thePopUpButton menu] == menu && ![[menu itemArray] containsObject:_editMenuItem]) {
[menu addItem:[NSMenuItem separatorItem]];
[menu addItem:_editMenuItem];
}
}
In this example the _editMenuItem is an NSMenuItem property provided by the object implementing this NSMenuDelegate method. Eventually it could be something as this:
_editMenuItem = [[NSMenuItem alloc] initWithTitle:#"Edit…" action:#selector(openEditPopUpMenuVC:) keyEquivalent:#""];
// Eventually also set the target for the action: where the selector is implemented.
_editMenuItem.target = self;
You'll then implement the method openEditPopUpMenuVC: to present to the user the view responsible for editing the content of the popUpButton (in your case the CoreData objects provided via bindings).
The only problem I haven't yet solved with this approach is that when getting back from the view where the edit happens, the NSPopUpButton will have the new item "Edit…" selected, rather than another "valid" one, which is very inconvenient.

Interface-Builder: "combine" NSView-class with .xib

I'd like to set up a custom NSView in Interface-Builder, but I don't get it to work for OSX.
In my ViewController's .xib, I added a custom view and set the Class to MyCustomView. I created MyCustomView.h, MyCustomView.m and MyCustomView.xib.
In MyCustomView.xib, I set the Class to MyCustomView as well. In MyCustomView.m, - (void)awakeFromNib is called, but - (id)initWithCoder:(NSCoder *)aDecoder and - (id) awakeAfterUsingCoder:(NSCoder*)aDecoder aren't.
What I'd like to achieve is that in my ViewController, the view I added is "filled" with the view I set up in MyCustomView.xib. What's the best way to do that?
EDIT: I don't think I was clear enough...
I've got my ViewController containing a Custom View called MyCustomView.
This view should be of type MyCustomView, where
MyCustomView.h
MyCustomView.m
MyCustomView.xib
exists. I already set the File's Owner of MyCustomView.xib to MyCustomView and I already set the CustomView in my ViewController to MyCustomView - but it doesn't work.
If I do it with
- (void)awakeFromNib {
NSString* nibName = NSStringFromClass([self class]);
NSArray* topLevelObjects;
[[NSBundle mainBundle] loadNibNamed:nibName
owner:nil
topLevelObjects:&topLevelObjects];
NSView* view = topLevelObjects[0];
[view setFrame:[self bounds]];
[self addSubview:view];
}
I only get a view of type NSView, not MyCustomView... Is there no easy way to tell the ViewController.xib that it's a MyCustomView?
EDIT 2: I uploaded a simple project
At https://dl.dropboxusercontent.com/u/119600/Testproject.zip you find a simple project with the MyCustomView (not in a ViewController but in the window.xib) - but it doesn't show the button which is in MyCustomView.xib. I'd like to achieve exactly that - what's the simplest, best way?
EDIT - apologies, my existing answer failed to take into account the need to connect outlets and actions. This way should do it...
Given the files...
MyCustomView.h
MyCustomView.m
MyCustomView.xib
In MyCustomView.h
declare IBOutlets for your interface elements. You need at least one, to hold a pointer to the top-level view in the xib file
#property (nonatomic, strong) IBOutlet NSView *view;
In MyCustomView.xib
ensure that there is only one top-level view
set file's owner class to MyCustomView in the Identity Inspector
ensure that the top-level view is set to the default NSView class.
now you can connect IBOutlets declared in MyCustomView.h to interface objects in the xib file. At very least you need to connect up the top-level view to your view outlet.
In MyCustomView.m:
- (id)initWithFrame:(NSRect)frame
{
NSString* nibName = NSStringFromClass([self class]);
self = [super initWithFrame:frame];
if (self) {
if ([[NSBundle mainBundle] loadNibNamed:nibName
owner:self
topLevelObjects:nil]) {
[self.view setFrame:[self bounds]];
[self addSubview:self.view];
[self.myCustomButton setTitle:#"test success"];
}
}
return self;
}
In your window's xib file, add a custom NSView and change it's class to MyCustomView.
in OSX prior to 10.8 the method loadNibNamed was a class method - use it instead if you need backwards compatibility, but it is deprecated now:
[NSBundle loadNibNamed:#"NibView" owner:self]
Note that MyCustomView.xib's view is NOT MyCustomView's view, but the sole subview of it's view (this is similar to the way a tableViewCell possesses a single contentView).
In the project sample you have posted, you need to make the following changes:
in MyCustomView.h
. add an NSView property
in MyCustomView.xib:
. change the top-level view from MyCustomView custom class to NSView (the default)
. set the File's Owner to MyCustomView.
. connect IBOutlets from File's owner's view and myCustomButton to interface view and button
. for testing make the view a lot smaller and push the button up to the top right (you won't see it in your window as it is here)
in MyCustomView.m:
. replace all of your implementation code with the initWithFrame method here
In order to load a custom subclass of a view or a control (or any other class that can be used in Interface Builder for that matter), you need to add the base version (NSView in your case, and as you have done), then select that object in the window and go to the Identity Inspector (Cmd-Opt 3).
Instead of the pre-defined value for Class (NSView, in your case), type in the name of your custom subclass. Voilà!
A small detail related to the IB UX is that you'll probably have to move the focus from the input field in order for that change to be registered when you build and run the app.

Synching Core Data with Two Xib Simultaneously

I am trying to create a Core Data app where the user is organizing a lot of information into sections. I have a main xib that has a popup menu and a non-bordered box. Into that box, a separate xib will be loaded with the view for the section chosen from the popup button.
I decided to make a second window/panel that's a sort of accessory window. The idea is that the main window shows a summary table, while the accessory view makes it easier to input data by taking the current selection in the summary table and displaying it in text fields, graphical date pickers (instead of forcing the user to use the correct format for typing a date into the table), etc. It also holds some optional fields and displays stats, so those don't clog up my main view.
My Document.m for the main xib has:
- (id)init
{
self = [super init];
if (self) {
viewControllers = [[NSMutableArray alloc] init];
accessoryViewControllers = [[NSMutableArray alloc] init];
ManagingViewController *vc;
ManagingViewController *accessoryVC;
vc = [[SummaryViewController alloc] init];
accessoryVC = [[SummaryAccessoryViewController alloc] init];
[vc setManagedObjectContext: [self managedObjectContext]];
[accessoryVC setManagedObjectContext: [self managedObjectContext]];
[viewControllers addObject: vc];
[accessoryViewControllers addObject: accessoryVC];
}
return self;
}
And so on for the other viewControllers/xib files that will be listed in the popup button. Making a selection in the popup returns its sender tag, then calls another method that takes the tag, and loads the objectAtIndex in the vc array into the main window box and accessoryVC array into the accessory window. In the actual SummaryViewController.m I have:
- (id) init {
self = [super initWithNibName: #"SummaryView" bundle:nil];
if (self) {
[self setTitle: #"Summary"];
}
return self;
}
I built all the views, then started binding. A column in the table in the main window might be bound to arrangedObjects.aaa and the accessory view's textfield will be bound to selection.aaa, but its selection won't change when the tableview selection changes. I'm guessing that's because technically they're using two separate NSArrayControllers.
I've seen examples in books where a secondary window had data synched to the main window, and it worked because both windows came from the same xib, so they used the same NSArrayController. My question is, which of these options can I use:
1) Is there a way to make the NSArrayControllers stay in synch across multiple xib files?
2) I could move the custom view in the SummaryAccessoryView.xib into SummaryView.xib so that the one xib contains both the view for the main and accessory windows. Then they would share NSArrayControllers. But then how do I get my popup to put one view in the main window and the other in the accessory window? My current method relies on [super initWithNibName: SummaryView.xib] so I don't see any way to specify which view.
3) I guess I could cave and rebuild the whole thing to a one-window model, scrap the redundant fields and put the extra fields at the bottom part of my main view, but the user won't be able to hide it or move it around and I have that issue again with having a user formatting their dates into a tableview... It might work if I knew how to have a graphical date picker come up when the user clicks a table cell. But I'd prefer to keep the two-window model if possible.
Any ideas on how to do option 1 or 2?
EDIT: I got option 3 working:
You need a few ivars first: a date picker (myDatePicker), your table (myTable), the pop-over that houses the date picker (myPopover), and the NSArrayController (myArray). Also in my example, the date column is the first column (column 0) and I've named it in IB as "date". If you have multiple dates (like start/end dates or two tables), you can add in an NSString ("tableAndColumn") that uses #define to set flags to identify which date you need, and turn your if statement into an if-else with multiple cases.
- (BOOL) tableView:(NSTableView *)tableView
shouldEditTableColumn:(NSTableColumn *)tableColumn
row:(NSInteger)row {
if (tableColumn == [myTable tableColumnWithIdentifier: #"date"]) {
//tableAndColumn = myStartDate;
[myDatePicker setDateValue: [myArray valueForKeyPath: #"selection.date"]]; //this will set your date picker to the value already in the table column
NSRect rect = [myTable frameOfCellAtColumn: 0 row: [myTable selectedRow]];
[myPopover showRelativeToRect: rect ofView: myTable preferredEdge:NSMaxYEdge];
return NO;
// } else if (tableColumn == [myTable tableColumnWithIdentifier: #"endDate"]) {
// ...
} else {
return YES;
}
}
- (void) popoverWillClose:(NSNotification *)notification {
// if ([tableAndColumn isEqualToString: MyStartDate]) {
[myArray setValue: [myDatePicker dateValue] forKeyPath: #"selection.date"];
// } else if ([tableAndColumn isEqualToString: MyEndDate]) {
// ...
// }
}
You can bind to your array controllers across NIB files by using properties of your NIB file's owner that are key-value coding and key-value observing compliant. E.g. if one of your NIB files has your NSViewController subclass as the file's owner, you can bind controls to the file's owner using key paths that start with representedObject.
In your example, you could store your view controllers (which you initialized in -[Document.m init]) in dedicated properties, and set the NSViewController's representedObject to the document instance. Then, in your NIB file, you could bind your controls to the file's owner using a key path that starts with representedObject.myViewControllerProperty.myArrayControllerProperty etc.
In my own app, I initiate a custom window controller in -[Document makeWindowControllers] using -initWithWindowNibName and store it in a mainWC property. This main window controller creates subordinate view controllers (similar to how you've done it) and sets their representedObject property to itself ([vc setRepresentedObject:self]). All bindings in other NIB files are then routed thru this main window controller via bindings to file's owner using key paths that start with representedObject.
In a similar fashion, my MainMenu.xib file connects e.g. the "Enabled" property of some menu commands to appropriate array controller properties by binding to the Application object using key paths that start with mainWindow.windowController.document.mainWC.

How do I set a default sort order for an NSTableView?

I've got a cocoa app that's got a TableView with bindings to a model through an NSArrayController.
The app works as I want, but the default sort order of the table is wrong.
buildwatch http://public.west.spy.net/BuildWatch.png
I typically start the program and click on the last header twice to get it sorting the right way. Is there a way in the nib/bindings/whatever to specify the default sort order, or to programatically tell it to do what would happen if I clicked there twice? Or even just remember the previous sort order?
Look at NSSortDescriptor.
You can set it up using -setSortDescriptors: on the NSTableView. Or you can put the sort descriptors in an ivar and bind them with the Sort Descriptor binding in IB.
I typically do this sort of thing in -windowDidLoad. Suppose your NSWindowController subclass has the IBOutlet _arrayController set to the NSArrayController in question, and that your model possesses the property buildETA:
NSSortDescriptor *buildETASortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"buildETA" ascending:NO];
[_arrayController setSortDescriptors:[NSArray arrayWithObject:buildETASortDescriptor]];
[buildETASortDescriptor release];
Edit: Changed -awakeFromNib to -windowDidLoad since this is a hypothetical NSWindowController subclass
You could also create this custom class and then in InterfaceBuilder select your ArrayController and change Custom Class in the Indentity Inspector to your CustomArrayController
#import "CustomArrayController.h"
#implementation PatchbayArrayController
-(void)awakeFromNib
{
[super awakeFromNib];
NSSortDescriptor *mySortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"propertyNameHere" ascending:YES];
[self setSortDescriptors:[NSArray arrayWithObject:mySortDescriptor]];
[mySortDescriptor release];
}
#end

Resources