I've got an NSTableView bound to an NSArrayController via content and selection indexes. All great so far - content displayed, etc.
Now an NSSearchField is bound to the array controller via filterPredicate and the property of the array content instances that's to be searched.
Searching/filtering the table view works great; table view showing only matching entries.
However, searching resets the selection on the NSTableView if the existing selection isn't in the search results. Worse, not only during the search but after ending the search there's no selection on the table view.
The NSArrayController is set up to Avoid Empty Selection.
Yet, debugging the array controller's selection indexes shows that searching resets them to an empty set. Don't quite know what to make of it..
Any hints on how to properly configure bindings in this scenario to really prevent an empty selection much appreciated!
Unfortunately array controllers don't track and restore the selection as their arranged objects change. You'll have to do this yourself in code. You can keep track of the current selection by using KVO to observe the selection on the array controller. You can also observe the controller's arranged objects to know when it changes as a result of filtering. Upon every change just set the current selection back to the tracked value (assuming its still in arranged objects) or set the selection to a new value.
Related
I'm looking for a way to filter the items displayed in an NSCollectionView. My NSCollectionView is managed using the 10.11 and up mechanism (i.e. with NSCollectionViewDataSource and NSCollectionViewDelegate). I can't seem to find any documentation on how this is done.
Is it possible to implement a filter for the visible items, and if so, is there documentation available to show me how it is done?
There is probably a better way, but until that comes along here is the workaround I found that doesn't require modifying the data source (which seems slightly cleaner conceptually, as the data shouldn't change during filtering, just which items are rendered).
Assign a NSCollectionViewDelegateFlowLayout as your NSCollectionView delegate. Then implement the method collectionView(NSCollectionView, layout: NSCollectionViewLayout, sizeForItemAt: IndexPath) (docs). This allows you to specify the size per item during layout of the collection view. I simply returned a 0 width and height CGSize for the items which did not match the current filter string. Otherwise return the ItemSize of the given layout. On every action of the search field (e.g. every keystroke), I'm calling ReloadData() on the collection view, which causes a re-layout.
One issue with this is that you have to set a minimum inter-item and line spacing of 0, otherwise the 0-sized items will still get the inter-item spaces, which can produce unevenly spaced visible items.
I'd still love to know a better way, so if future readers have one, I'll change the accepted answer.
AppKit Release Notes for OS X v10.11 suggests that collection view items can be resized on a per-item basis:
Item size can be determined globally for all of a CollectionView’s items (by setting an NSCollectionViewFlowLayout’s “itemSize” property), or can be varied from one item to the next (by implementing -collectionView:layout:sizeForItemAtIndexPath: on your CollectionView’s delegate).
In my case, my CollectionViewItem consists of a single label that contains a string of varying length. I'm using the NSCollectionView to display an array of strings, as NSStackViews don't support array bindings, and don't flow to new lines. The array of strings is bound to the NSCollectionView's content via an array controller.
My item's nib file is properly set up, the root view and the label both have Content Hugging and Content Compression Resistance Priorities of 1000, and the edges are aligned via AutoLayout.
Now, the NSCollectionViewLayout's delegate method has this signature:
func collectionView(collectionView: NSCollectionView,
layout collectionViewLayout: NSCollectionViewLayout,
sizeForItemAtIndexPath indexPath: NSIndexPath) -> NSSize
My idea now is to grab the item itself, run a layout pass over it, and then returning the new item size.
let item = collectionView.itemAtIndexPath(indexPath)!
item.view.layout()
return item.view.bounds.size
Problem with this approach is that itemAtIndexPath returns nil. If I return a default size in the nil case, that default size is used for all cells.
How can I set a NSCollectionView to respect my item's AutoLayout constraints and for each cell, use the computed size dynamically?
There's a duplicate of this question that I answered but this is probably the one it should be directed to since it's older.
Juul's comment is correct- the items do not exist. sizeForItemAt being called is the collection asking the delegate for any specific sizing for that data entry with which it will use to help create its eventual view controller, NSCollectionViewItem. So you create a loop when you ask the collection to get you an item in the method that it uses to help get an item.
The problem we have is that we want sizing based on the appearance of that data: the length of a text label with proper formatting, not just, say, the string length. So we hit a chicken and egg problem.
The only solution I've come to, which could be prettier, is the following:
Prep
Subclass NSCollectionViewItem and ensure your collection view has a data source that returns the proper subclassed item.
Use constraints in your XIB, completely.
Your subclass should have a method that loads in the data object to be represented- both for this and of course your data source protocol methods.
At some point prior to the first sizeForItemAt call, or at the beginning of the first one if you hadn't by then, manually create an instance of your NSCollectionViewItem subclass, and use NSNib's instantiate(withOwner:topLevelObjects:) to instantiate its XIB with your subclass as an owner. Store that reference as a sort of "sizing template," so you only need to do it once. Delegate was easiest spot for me.
^Note: my first route was to attempt this through the collection's makeItemWithIdentifier, but it was more brittle as it required the collection to have items at the time of creating the sizing template. It also could not be done during an initial sizeForItemAt (accessing/making items during a reload crashes). And I was worried that because it was made with the collection it may get reused down the line and the methods below don't work or start editing visible items. YMMV.
In sizeForItemAt
Directly get the data object being represented from the datasource. Have your sizing template object represent that data object with the method I mentioned earlier.
Access the sizing template's View.FittingSize, the smallest size an item can be given its constraints/priorities, and return that.
Bam! Hasn't been stress tested or anything but no problems on my end, and its not doing a layout pass or anything, just calling FittingSize. I haven't seen this articulated anywhere online yet, so I wanted to write out the full explanation.
I did this in Xamarin.Mac, so my code won't be 1:1 and I don't want to write garbled swift and mess anything up.
TLDR: manually instantiate a NSCollectionViewItem subclass and its xib that you will store, unowned by the collection. During sizeForItem populate that item you store as a sizing reference, and return the FittingSize of the collection item's view.
Perhaps using an NSTableView as a form is a terrible idea, but I've got it working pretty well in every respect except that you can't tab from field to field.
I have a table with a label column and a field column. I also have an array that keeps track of each row in the table with its accompanying NSTextField control. The controls are set in the array as they are loaded during tableView(tableView: viewForTableColumn: row:).
After the tableview is drawn, I run through each control in the array and set its nextKeyView to the following control.
This works swell, BUT only for those controls which have been displayed on screen. Because the control isn't added until it's loaded, table rows which are off-screen aren't hooked up.
My current approach (which is awful) is to manually scroll the table several times in order to force everything to load, then set all the nextKeyViews. I haven't got it working very well yet, so I was hoping someone had a better idea. Force-loading all views, if it works, should be an OK solution, but there may be something smarter out there too.
I guess the answer is
table.scrollRowToVisible(<row>)
This will show the row you want to see.
What I want to accomplish seems like it should be fairly straightforward. I have placed a sample project here.
I have a NSArrayController filled with an array of NSDictionaries.
[[self controller] addObject:#{ #"name" : #"itemA", #"part" : #"partA" }];
[[self controller] addObject:#{ #"name" : #"itemB", #"part" : #"partB" }];
[[self controller] addObject:#{ #"name" : #"itemC", #"part" : #"partC" }];
I am populating a NSPopupButton with the items in this array based on the 'name' key. This is easily accomplished with the following bindings
I would then like to populate a NSTextField with the text in the 'part' key based on the current selection of the NSPopupButton. I have setup the following binding:
With these bindings alone, the text field does display 'partC'.
However, if I change the value of the NSPopupMenu, what the text field shows does not change.
I thought this would simply be a matter of setting up the 'Selected Object' binding on the NSPopupButton
but that isn't working. I end up with the proxy object in my menu for some strange reason (providing the reason why would be a bonus).
So, what do I need to do to make this work?
Don't use "Selected Object" in this case. Bind the pop-up's "Selected Index" binding to the NSArrayController's selectionIndex Controller Key. Tried it out on your sample project and it works.
EDIT:
You asked why it's appropriate to use selectionIndex over selectedObject. First some background:
When binding a popup menu, there are three virtual "Collections" you can bind: Content is the abstract "list of things that should be in the menu" -- you must always specify Content. If you specify neither Content Objects, nor Content Values, then the collection of values bound to Content will be used as the "objects" and the strings returned by their -description methods will be used as the "values". In other words, the Content Values are the strings displayed in the pop-up and the Content Objects are the things they correspond to (which are possibly not strings, and which might not have a -description method suitable for generating the text in the pop-up). What's important to realize here is that there are potentially three different 'virtual arrays' in play here: The array for Content, the array for Content Objects (which may be different) and the array for Content Values (which may also be different). They will all have the same number of values, and typically, the Content Objects and Content Values will be functions (in the mathematical sense) of the corresponding items in the Content array.
The next thing that's important to realize is that part of NSArrayController's purpose in life is to keep track of the user's selection. This is only mildly (if at all) interesting in the case of a pop-up, but starts to become far more interesting in the case of an NSTableView. Internally, NSArrayController keeps track of this by keeping an NSIndexSet containing the indexes in the Content array that are selected at any given time. From there, selection state is exposed in several different ways for your convenience:
selectionIndexes is as described - an NSIndexSet containing the indexes of the selected items in the Content array
selectionIndex is a convenient option for applications that do not support multiple selection. It can be thought of as being equivalent to arrayController.selectionIndexes.firstIndex.
selectedObject is also useful in single selection cases, and corresponds conceptually to ContentObjectsArray[arrayController.selectionIndexes.firstIndex]
selection returns a special object (opaque to the consumer) that brokers reads and writes back to the underlying object (or objects in the case of multiple selection) in the Content Array of the array controller. It exists to enable editing multiple objects at a time in multiple selection cases, and to provide support for other special cases. (You should think of this property as read-only; Since its type is opaque to the consumer, you could never make a suitable new value to write into it. It's meaningful to make calls like: -[arrayController.selection setValue: myObject forKey: #"modelKey"], but it's not meaningful to make calls like -[arrayController setValue: myObject forKey: #"selection"]
With that understanding of the selection property, let's take a step back and see why it's not the right thing to use in this case. NSPopUpButton tries to be smart: You've provided it with a list of things that should be in the menu via the Content and Content Values bindings. Then you've additionally told it that you want to bind its Selected Object to the the NSArrayController's selection property. You're probably thinking of this as a "write only" binding -- i.e. "Dear pop-up, please take the user's selection and push it into the arrayController", but the binding is really bi-directional. So when the bindings refresh, the popup first populates the menu with all the items from the Content/Content Values bindings, and then it says, "Oh, you say the value at arrayController.selection is my Selected Object. That's odd -- it's not in the list of things bound with my Content/Content Values bindings. I'd better add it to the list for you! I'll do that by calling -description on it, and plunking that string into the menu for you." But the object you get from that Selected Object binding is the opaque selection object described above (and you can see from the outcome that it is of class _NSControllerObjectProxy, a private-to-AppKit class as hinted by the leading underscore).
In sum, that's why binding your popup's Selected Object binding to the array controller's selection controller key is the wrong thing to do here. Sad to say, but as I'm sure you've discovered, the documentation for Cocoa bindings only begins to scratch the surface, so don't feel bad. I've been working with Cocoa bindings pretty much daily, in a large-scale project, for several years now, and I still feel like there are a lot of use cases I don't yet fully understand.
I'm implementing an example, in that example, I read in data from a database, put it in an array, sort it, and it's displayed using the RootViewController. The DB read and array load happen before the RVC code. So, it works, I get the data in the window created by the RVC and there's a nav controller there as well.
I want to add a button or something to the nav controller so that when you hit it, it sends a value back to the RootViewController.m file, then based on that value, I want to resort the array and display it once again in the RootViewController window.
I'm not sure how to do this. What changes would I have to make to the .xib and the RootViewController.m file?
Please, I'm a confused nube. %-0 Thank you very much.
There's a fair amount to this, so I'll give some general points and if any of them cause problems, it may be easier to work out smaller details.
In you RVC's viewDidLoad method, you can create a button and set it as the right or left button in your controller's navigationItem.
You can associate a tap on that button with a method in your controller that can do whatever you want when the button is tapped. (But a button doesn't send values, really, so you may have to explain more about that idea.)
I assume the RVC has a table view if you're showing array contents, so once the array (mutable array, I'd assume) is re-sorted, you can tell the table view to reload its data.
In answer to your secondary question, once you have resorted your array (or generally updated your data however you wish) you can force the table view to reload programmatically
[tableView reloadData];
Where 'tableView' is your instance variable pointing to your table view