Dynamically hiding columns in a NSTableView - cocoa

I want to dynamically hide/show some of the columns in a NSTableView, based on the data that is going to be displayed - basically, if a column is empty I'd like the column to be hidden. I'm currently populating the table with a controller class as the delegate for the table.
Any ideas? I see that I can set the column hidden in Interface Builder, however there doesn't seem to be a good time to go through the columns and check if they are empty or not, since there doesn't seem to be a method that is called before/after all of the data in the table is populated.

In Mac OS X v10.5 and later, there is the setHidden: selector for NSTableColumn.
This allows columns to be dynamically hidden / shown with the use of identifiers:
NSInteger colIdx;
NSTableColumn* col;
colIdx = [myTable columnWithIdentifier:#"columnIdent"];
col = [myTable.tableColumns objectAtIndex:colIdx];
[col setHidden:YES];

I've done this with bindings, but setting them up programmatically instead of through Interface Builder.
This psuedo-snippet should give you the gist of it:
NSTableColumn *aColumn = [[NSTableColumn alloc] initWithIdentifier:attr];
[aColumn setWidth:DEFAULTCOLWIDTH];
[aColumn setMinWidth:MINCOLWIDTH];
[[aColumn headerCell] setStringValue:columnLabel];
[aColumn bind:#"value"
toObject:arrayController
withKeyPath:keyPath
options:nil];
[tableView addTableColumn:aColumn];
[aColumn release];
Of course you can add formatters and all that stuff also.

It does not work in the Interface Builder. However it works programatically. Here is how I bind a NSTableViewColumn with the identifier "Status" to a key in my NSUserDefaults:
Swift:
tableView.tableColumnWithIdentifier("Status")?.bind("hidden", toObject: NSUserDefaults.standardUserDefaults(), withKeyPath: "TableColumnStatus", options: nil)
Objective-C:
[[self.tableView tableColumnWithIdentifier:#"Status"] bind:#"hidden" toObject:[NSUserDefaults standardUserDefaults] withKeyPath:#"TableColumnStatus" options:nil];

I don't have a complete answer at this time, but look into Bindings. It's generally possible to do all sorts of things with Cocoa Bindings.
There's no Visibility binding for NSTableColumn, but you may be able to set the width to 0.
Then you can bind it to the Null Placeholder, and set this value to 0 - but don't forget to set the other Placeholders to reasonable values.
(As I said, this is just a start, it might need some tweaking).

A NSTable is just the class that paints the table. As you said yourself, you have some class you give the table as delegate and this class feeds the table with the data to display. If you store the table data as NSArray's within your delegate class, it should be easy to find out if one column is empty, isn't it? And NSArray asks your class via delegate method how many columns there are, so when you are asked, why not looking for how many columns you have data and report that number instead of the real number of columns you store internally and then when being asked for providing the data for (column,row), just skip the empty column.

There is no one time all the data is populated. NSTableView does not store data, it dynamically asks for it from its data source (or bound-to objects if you're using bindings). It just draws using data it gets from the data source and ditches it. You shouldn't see the table ask for data for anything that isn't visible, for example.
It sounds like you're using a datasource? When the data changes, it's your responsibility to call -reloadData on the table, which is a bit of a misnomer. It's more like 'invalidate everything'.
That is, you should already know when the data changes. That's the point at which you can compute what columns should be hidden.

#amrox - If I am understanding your suggestion correctly, you're saying that I should bind a value to the hidden property of the NSTableColumns in my table? That seems like it would work, however I don't think that NSTableColumn has a hidden property, since the isHidden and setHidden messages control the visibility of the column - which tells me that this isn't a property, unless I'm missing something (which is quite possible).

I would like to post my solution updated for Swift 4 using Cocoa bindings and the actual isHidden flag without touching the column widths (as you might need to restore the original value afterwards...). Suppose we have a Checkbox to toggle some column visibility (or you can always toggle the hideColumnsFlag variable in the example below in any other way you like):
class ViewController: NSViewController {
// define the boolean binding variable to hide the columns and use its name as keypath
#objc dynamic var hideColumnsFlag = true
// Referring the column(s)
// Method 1: creating IBOutlet(s) for the column(s): just ctrl-drag each column here to add it
#IBOutlet weak var hideableTableColumn: NSTableColumn!
// add as many column outlets as you need...
// or, if you prefer working with columns' string keypaths
// Method 2: use just the table view IBOutlet and its column identifiers (you **must** anyway set the latter identifiers manually via IB for each column)
#IBOutlet weak var theTableView: NSTableView! // this line could be actually removed if using the first method on this example, but in a real case, you will probably need it anyway.
// MARK: View Controller Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
// Method 1
// referring the columns by using the outlets as such:
hideableTableColumn.bind(.hidden, to: self, withKeyPath: "hideColumnsFlag", options: nil)
// repeat for each column outlet.
// Method 2
// or if you need/prefer to use the column identifiers strings then:
// theTableView.tableColumn(withIdentifier: .init("columnName"))?.bind(.hidden, to: self, withKeyPath: "hideColumnsFlag", options: nil)
// repeat for each column identifier you have set.
// obviously use just one method by commenting/uncommenting one or the other.
}
// MARK: Actions
// this is the checkBox action method, just toggling the boolean variable bound to the columns in the viewDidLoad method.
#IBAction func hideColumnsCheckboxAction(_ sender: NSButton) {
hideColumnsFlag = sender.state == .on
}
}
As you may have noticed, there is no way yet to bind the Hidden flag in Interface Builder as on XCode10: you can see the Enabled or Editable bindings, but only programmatically you will have access to the isHidden flag for the column, as it is called in Swift.
As noted in comments, the second method relies on the column identifiers you must manually set either via Interface Builder on the Identity field after selecting the relevant columns or, if you have an array of column names, you can enumerate the table columns and assign the identifiers as well as the bindings instead of repeating similar code lines.

I found a straightforward solution for it.
If you want to hide any column with the Cocoa binding technology:
In your instance of the NSArrayController, create an attribute/parameter/slot/keyed value which will have NSNumber 0 if you want a particular column to be hidden and any value if not.
Bind the table column object's maxWidth parameter to the data slot, described in (1). We will use the maxWidth bound parameter as a message receiver.
Subclass the NSTableColumn:
import Cocoa
class Column: NSTableColumn {
/// Observe the binding messages
override func setValue(_ value: Any?, forKey key: String) {
if key == "maxWidth" && value != nil { // Filters the signal
let w = value as! NSNumber // Explores change
if w == NSNumber(integerLiteral: 0) {
self.isHidden = true
} else {
self.isHidden = false
}
return // No propagation for the value change
}
super.setValue(value, forKey: key) // Propagate the signal
}
}
Change the class of the column to Column.

Related

Separate NSPopUpButton content from label while using bindings

I have an NSPopupButton whose content is bound to an NSArray, let’s say the array is
#[
#"Option 1",
#"Option 2"
];
Its selected object is bound to User Defaults Controller, and is written to a preference file by the user defaults system.
In my code I check whether the preference is set to #"Option 1" or not, and perform actions accordingly.
This all worked well (though I did feel a little uneasy checking for what is essentially a UI value, but whatever...) until I needed to localize.
Because the value is the label, I’m having an issue.
If my user is in France, his preferences file will say #"L’option 1", which is not equal to #"Option 1". I need to abstract the presentation from the meaning and it's proving pretty difficult.
I split up the binding into two arrays, let's call them values and labels.
Let’s say they look like this:
values = #[
#"option_1",
#"option_2"
];
labels = #[
NSLocalizedString(#"Option 1", nil),
NSLocalizedString(#"Option 2", nil)
];
I’ve bound the NSPopUpButton’s Content binding to values and its Content Values binding to labels. However, the popup list is showing option_1 and option_2, it does not seem to want to use the labels array to label the items in the popup button.
How do I get the NSPopUpButton to use values internally and store that in the preferences file, but display labels to the user?
It doesn’t have to be architected this way, if you can think of a better solution. The point is I want to store and check one value, and have that value associated with a label that gets localized appropriately.
Cocoa bindings work very well with value transformers, because you can apply them directly in the bindings window, for example
#implementation LocalizeTransformer
+ (Class)transformedValueClass
{
return [NSArray class];
}
+ (BOOL)allowsReverseTransformation
{
return NO;
}
- (id)transformedValue:(id)value {
if (![value isKindOfClass:[NSArray class]]) return nil;
NSMutableArray *output = [NSMutableArray arrayWithCapacity:[value count]];
for (NSString *string in value) {
[output addObject:NSLocalizedString(string, nil)];
}
return [output copy];
}
#end
you have to register the transformer in awakeFromNib or better in +initialize
NSValueTransformer *localizeTransformer = [[LocalizeTransformer alloc] init];
[NSValueTransformer setValueTransformer:localizeTransformer
forName:#"LocalizeTransformer"];
then it appears in the popup menu of value transformers
Bind Selected Tag to your User Defaults Controller instead of Selected Object.
If the NSPopupButton choices are fixed add the NSMenuItems in Interface Builder and set their Tags. Otherwise bind an array of NSMenuItem, again with proper Tags.
Selected Index would also work but only until you change the order.

How to sort NSTableView in Swift using sortDescriptors?

I use view-based NSTableView in my Cocoa app which is written in Swift, and want to implement a sort functionality on two table columns. However, in Objective-C, you can implement it by first setting the "Sort Key" in Attribute Inspector, and then implement the data source delegate method named tableView: sortDescriptorsDidChange:.
However, this method takes sortDescriptor as a parameter and lets developers use it within the method, like so:
- (void) tableView:( NSTableView *) tableView sortDescriptorsDidChange:( NSArray *) oldDescriptors {
[self.songs sortUsingDescriptors:tableView.sortDescriptors];
[tableView reloadData];
}
However, in Swift Array, there are no such method as sortUsingDescriptors. So I first tried to convert Array to NSMutableArray in order to use the NSMutableArray's method, but since my Swift Array is defined as AnyObject[], it cannot be casted to NSMutableArray.
So how should I implement the sort functionality to the table view in Swift? I know Swift Array can use sort function to sort the object by comparing the two arguments and returning bool values, but is it possible to use sortDescriptors to sort the table? Or should I just ignore the sortDescriptors argument and instead write my own sort logic manually? (but then I don't know how to tell what column is clicked without the sortDescriptors value).
Probably the best way, at least right now, is to first convert it to NSMutableArray and then sort it using NSMutableArray's sortUsingDescriptors method, and finally convert it back to the original Array, like so:
func tableView(tableView: NSTableView!, sortDescriptorsDidChange oldDescriptors: [AnyObject]) {
var songsAsMutableArray = NSMutableArray(array: songs)
songsAsNSMutableArray.sortUsingDescriptors(tableView.sortDescriptors)
songs = songsAsNSMutableArray
tableView.reloadData()
}
By the way, var songsAsMutableArray = songs as NSMutableArray causes an error: NSArray is not a subtype of NSMutableArray, so I created an NSMutableArray instance as shown above.

Cannot assign to the result of this expression when setting table view's dataSource

I want to use NSTableView in my Cocoa app and hence I have to set the table view's NSTableViewDataSource delegate to self. However, the following code doesn't compile due to the error Cannot assign to the result of this expression.
myTable.dataSource = self
And I did declare the myTable variable as like this:
#IBOutlet var myTable: NSTableView
Also note that I correctly declare my AppDelegate conforms to NSTableViewDataSource protocol.
So what's wrong with the above two lines?
Also, is it correct that I declare the table view as #IBOutlet var myTable: NSTableView? Or should I define it as optional or implicitly unwrapped optional variable?
It looks as though NSTableView has not been fully modernized. Its dataSource property is still informal, existing as just accessor methods, rather than being a declared property using #property. Therefore, the Swift interface doesn't have a dataSource property. It has the methods dataSource() and setDataSource().
So, try:
myTable.setDataSource(self)
IBOutlet properties are automatically made weak implicitly unwrapped optionals that are initially set to nil.
You can directly access the value safely using an if let:
if let table = myTable{
table.dataSource = self
}

NSButton setAction selector

I just want to add a NSButton with setAction Arguments.
NSRect frame = NSMakeRect(10, 40, 90, 40);
NSButton* pushButton = [[NSButton alloc] initWithFrame: frame];
[pushButton setTarget:self];
[pushButton setAction:#selector(myAction:)];
But I want to put an argument to the function myAction...
How ?
Thanks.
But I want to put an argument to the function myAction...
How ?
You can't.
… if there is more than one button that uses this method, we can not differentiate the sender (only with title)...
There are three ways to tell which button (or other control) is talking to you:
Assign each button (or other control) a tag, and compare the tags in your action method. When you create controls in a nib, this has the downside that you have to write the tag twice (once in the code, once in the nib). Since you're writing out the button by hand from scratch, you don't have that problem.
Have an outlet to every control that you expect to send you this message, and compare the sender to each outlet.
Have different action methods, with each control being the only one wired up to each action. Each action method then does not need to determine which control sent you that message, because you already know that by which method it is.
The problem with tags is the aforementioned repetitiveness. It's also very easy to neglect to name each tag, so you end up looking at code like if ([sender tag] == 42) and not knowing/having to look up which control is #42.
The problem with outlets is that your action method may get very long, and anyway is probably doing multiple different things that have no business being in the same method. (Which is also a problem with tags.)
So, I generally prefer the third solution. Create an action method for every button (or other control) that will have you as its target. You'll typically name the method and the button the same (like save: and “Save”) or something very similar (like terminate: and “Quit”), so you'll know just by reading each method which button it belongs to.
I never programatically created an NSButton, but I think that you just need to create a method like this:
- (void) myAction: (NSButton*)button{
//your code
}
And that's it !!
You can use associated Objects for passing arguments.
You can refer : http://labs.vectorform.com/2011/07/objective-c-associated-objects/
http://www.cocoanetics.com/2012/06/associated-objects/
.tag should be sufficient if your object have any integer uniqueID.
I use .identifier instead, since it support string based uniqueID.
Example:
...
for (index, app) in apps.enumerated() {
let appButton = NSButton(title: app.title, target: self, action: #selector(appButtonPressed))
appButton.identifier = NSUserInterfaceItemIdentifier(rawValue: app.guid)
}
...
#objc func appButtonPressed(sender: NSButton) {
print(sender.identifier?.rawValue)
}

UIPickerView with one object

I am using a UIPickerView and currently their is only a single object in it. How can I display that single object on label.
It has this weird property that when we use pickerView the data is not set selected by default.Once we choose another object or roll it, then only any particular object is selected. Hence if only one object is their in pickerView. It does not count as selected even though when you tap on that single object.
I tried a lot but found that if their are more than one object then only you can display the selected object on label but not if their is only one object.
You need to make a code that is triggered when the UIPickerView changes, like this:
#pragma
-(void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component
//Action that triggers following code:
{
NSString *nameString = [nameOnMutubaleArrayYouWannaGetDataFrom objectAtIndex:[picker selectedRowInComponent:0]]; //Or 1 if u have multiple rows
NSString *labelString = [[NSString alloc]initWithFormat:
#" %# ", nameString];
labelOutput.text = labelString;
Hope this helps.

Resources