NSArray ArrayController SelectedObjects nil - cocoa

I have a coredata app with an Entity called Product.
In my interface I have an array controller for this entity called ProductAC. I have bound a table view and text fields to the array controller and can see the details, and I can add objects to it.
Now I want to create an export file for some of the objects. In the tableview I can select them, and then in the code I have this:
NSArray *uploadProducts = [ProductAC selectedObjects];
NSEnumerator *loop = [uploadProducts objectEnumerator];
This produces a nil value for the uploadProducts array.
How do I select the items to further process them? I have cleaned the file, closed and re-opened Xcode but I cannot seem to 'grab' the selected objects from the tableview?
thanks

Make sure you have the table view selectionIndexes property bound to the corresponding property on your array controller.

Related

Cocoa Bindings for hierarchical model

I have a NSCollectionView based master-detail interface,
where I want to display Boards in the master and Lists+Cards in the detail view.
Board, holds a NSMutableArray property lists of type List
List, holds a NSArray property cards of type Card
Card, has a NSString property name
The relationship is thus Board --> to-many List --> to-many Card
The master interface is fine.
The detail interface gets populated with corresponding Lists' titles
for a Board. Within the detail interface I also want to populate a NSPopupButton with the
cards for every list.
Problem: the NSPopupButton is empty.
Output: [<__NSArrayI 0x60000007b240> addObserver:forKeyPath:options:context:] is not supported. Key path: name
So after reading KVO, KVC and the Bindings documentation I am not sure if I need to do manual KVO for this sort of hierarchical model. Also the output hints that the name property is not KVC/KVO compliant, but it's just a NSString?
Do you suggest using an NSTreeController for this?
Bindings are setup like so:
BoardArrayController -> bound to File's owner
** Model key path: boards
ListArrayController -> bound to BoardArrayController
** Controller key: arrangedObjects
** Model key path: lists
** Mode: Class
CardArrayController -> bound to ListArrayController
** Controller key: arranged Objects
** Model key path: cards
** Mode: Class
The NSPopupButton has
Controller key for Content: arrangedObjects
Controller key for Content Value: arrangedObjects and model key path: name
Suggestions please
If I understand properly, in the master interface, the user selects a Board. Then, the detail interface should show the selected Board's lists. If so, the ListArrayController should be bound to BoardArrayController, controller key selection (not arrangedObjects), model key path lists.
Similarly, the CardArrayController should be bound to ListArrayController, controller key selection, model key path cards. Although it's not clear to me if the user has to first select a List and then sees a pop-up with that List's cards or if the pop-up is present in each item in the second collection view. If that's the case, then you'll need a separate array controller for each item, which is easiest if the item view is in a separate NIB.
If each list is the representedObject for each view item in the collection view, then you can populate each popupButton with a readonly NSArray property dependent upon the cards array that is in each list. In the List class add arrangedCards as a property.
- (NSArray *)arrangedCards
{
return [[self valueForKey:#"cards"] sortedArrayUsingDescriptors:
[self arrangedCardsSortDescriptors]];
}
Use the sort you want for the popup. This arranges by name.
- (NSArray *)arrangedCardsSortDescriptors
{
NSSortDescriptor *sortByName = [[NSSortDescriptor alloc] initWithKey:
#"name" ascending:YES];
return #[sortByName];
}
Bind the Content of the popup to the NSCollectionViewItem.
Model Key Path is representedObject.arrangedCards.
Use representedObject.arrangedCards.name as the Content Values.

NSComboBox bound to NSArrayController not updating

I have an issue with bindings between an NSComboBox element and an NSArrayController.
All bindings are setup in IB.
The NSComboBox element has the following bindings:
Content: bound to the NSArrayController instance, key: arrangedObjects
Content Values: bound to the NSArrayController instance, key: arrangedObjects.name
The NSArrayController element is bound in the following way:
Content Array: bound to File's Owner, key path: availableProperties (which is an NSMutableArray
In the code, I have a method which is called when the window opens and after some event fires.
The code is the following:
NSMutableArray* newAvailable = ...; //compute the new properties to be displayed.
//now I want to replace all the properties with the new one
if ([self.availableProperties count] > 0)
[self.availablePropertiesController removeObjects:self.availableProperties];
[self.availablePropertiesController addObjects:newAvailables];
where self.availableProperties is the NSMutableArray (the model) and self.availablePropertiesController is the NSArrayController
When the window opens the combo box is properly populated.
But when the event fires I execute the above statements, I can see the backing array correctly filled, but the combo box is completely empty.
Some ideas?
You’re close, you should just do:
NSMutableArray* newAvailable = ...; //compute the new properties to be displayed.
self.availableProperties = newAvailable;
You’ve already bound the arrayController’s to your ‘availableProperties’ variable, so all you have to do to change the UI is change your variable. That’s the beauty of bindings.
Also, your ‘availableProperties' should probably be an NSArray, not a NSMutableArray, because if you accidentally insert an object in the middle of the NSMutableArray the arrayController’s binding isn’t going to notice—it’ll only notice when the entire ‘availableProperties’ instance variable changes, not when something inside it mutates.

How can NSArrayController sort on a to-many relationship?

Using CoreData, I have an entity "Bookmark", that has an to-many relationship named 'tags' to another entity "Tag", and some commun attributes (string, date, ...).
In a NSTableView we display the Bookmarks entity via Binding:
the NSArrayController is binded to File's Owner.managedObjectContext
(standard XCode CoreData template, the managedObjectContext is in the AppDelegate)
The columns in TableView are binded to their respective attribute. In particular the Tag column is binded to this arrayController.arrangedObjects.tags with a subclass of NSValueTransformer so that we can show, as an NSString, a summary of the to-many relationship.
It work. Now when I click on column header the whole table view get sorted correctly except for the 'tag' column where I get this:
-[_NSFaultingMutableSet compare:]: unrecognized selector sent to instance
For sure the "Set" from this to-many relationship doesn't respond to the selector 'compare:'.
Question:
How can I make this work ? How can I sort on a to-many relationship ?
Are something like the ValueTransformer available ? If I could supply a custom class that would do the compare: for the ArrayController to know...
One possible hack: since _NSFaultingMutableSet is a NSSet, we can add the selector 'compare:' via a categorie.
#interface NSSet (someAdditions)
- (NSComparisonResult)compare:(NSSet *)anotherSet;
#end
#implementation NSSet (someAdditions)
- (NSComparisonResult)compare:(NSSet *)aSet {
...
}
#end
we can now implement this compare: selector as we wish, like comparing the count of each set, or their NSString representation in some way.
It work in my App. I re-enabled the 'Creates Sort Descriptor' on the binding of the NSTableColumn and can now click on the header of my tableView to sort.
It's a hack because it affect all NSSet... But at least I have my hook.
What do you think ?

How do you Sort Bound Data in NSTableColumn using InterfaceBuilder binding?

Sorting Bound Data in NSTableColumn using IB binding.
Keys: NSTableColumn , sorting, NSArrayController, Content Set
A contentSet serves like a data source for a TableColumn
This deals with a SplitView with two single column NSTableViews
The names of the TableViews are BookCategory and
Books.
The Books Table has a single column with book_titles.
The Class BookCategory has a one to many relationship
to Book.
The BookCategory Table is sorted at load using:
#implementation MyBookCategoryController
- (id)init
{
self = [super init];
if (self) {
// Initialization code here.
NSSortDescriptor *descript =
[NSSortDescriptor sortDescriptorWithKey:#"name"
ascending:YES selector:#selector(caseInsensitiveCompare:)];
[self setSortDescriptors:[[NSArray arrayWithObject:descript] autorelease] ];
}
return self;
}
This same approach fails to sort the BookTitle table at load. !!
The BookTitle table/column loads unsorted.
For the TableColumn the Attributes Inspector has
Sort Key:title
Selector: caseInsensitiveCompare:
Order: Ascending
This appears to enable the sorting when one clicks
on the column header.
I want the data sorted when the view loads.
The binding inspector for this book_title column has:
Value : BookCategoryArrayController.arrangedObjects.name
The BookTitleArrayController in binding inspector shows
Content Set: Book Category ArrayController.selection.books
To restate the problem, the tableview with the book titles
load unsorted. It sorts only after the user's FIRST click on the
column header.
Say there are three book categories Art, History, Sports.
When the app loads the left table in the splitview is sorted,
that is :
Art
History
Sports
When the user selects ANY category, titles for all books
in the category appear in the right tableView but unsorted.
If the user clicks on the Header of the book_title TableColumn
the initial sorting is done on the column. Thereafter
the selection of ANY book category causes a sorted display
of book_titles in the right tableView. That is, ONLY the first
selection of a category results in an Unsorted book title list.
Thanks a lot for reading, Mark
This is an outline of what finally worked for me.
Sorting with CategoryArrayController (NSArrayController)
Bindings Inspector
Content Set Bind to: CategoryArrayController.selection.books
SortDescriptors Bind to: ShareUserDefaultsController
Controller Key: values
sortDescriptors (note: exclamation point)
NSUnarchiveFromData
Shared User Defaults Controller
Referencing Outlets
userDefaultsController -- AppController (C:AppController)
Referencing Bindings
values.sortDescriptors -- Sort Descriptors Rx Array Controller (C:NSArrayController)
-- Sort Descriptors Category Array Controller (C:NSArrayController)
Clicking on Category Header does no sort. Selects all in Cats, and empties Recipe Table
Comments on the above welcome.
TableViewCat (NSTableController)
Attributes Inspector
TableView
Selection
Empty,Column,Type Select
Control
State.Enabled
Connections Inspec
None
Bindings Inspec
None
TableColumn -Category (NSTableColumn)
Attributes Inspec
Sort Key : None
Bindings Inspec
Value -- Category Array Controller.arrangedObjects.name
Sorting with RxArrayController (C:NSArrayController)
Attributes Inspec
Array Controller: 1-5 of 6
Object Controller
EntityName: Recipe
Prepares Content
Editable
Connections Inspector
Referencing Bindings
arrangedObjects.name -- Value - Table Column Recipe
selectedObjects -- Double Click Argument - Table View Book
Bindings Inspector
Controller Content
Content Set
Category Array Controller.selection.books
Controller Content Parameters
Sort Descriptors Bind to: ShareUserDefaultsController
Controller Key: values
sortDescriptors (note: exclamation point)
NSUnarchiveFromData
Parameters
Managed Object Context(App Delegate.manangedObjectContext
TableView Book (NSTableView)
Attributes Inspector
TableView
Selection Empty,Column,Type Select
Control
State.Enabled
Connections Inspec
Bindings
DoubleClick Argument -- Book Array Controller.selectedObjects
Double Click Target -- App Delegate.self
Bindings Inspec
Double Click Argument (Book Array Controller.selectedObjects)
Double Click Target (App Delegate.self)
Bind To: App Delegate
Model Key Path: self (Repeats ?)
Selector Name: newBookAction:
Conditionally Sets Enabled
TableColumn - Book (NSTableColumn)
Attributes Inspec
Sort Key : None
Connections Inspec
Bindings
Value -- Book Array Controller.arrangedObjects.name
Bindings Inspec
Value -- Book Array Controller.arrangedObjects.name

Dynamically hiding columns in a NSTableView

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.

Resources