I have several popup buttons whose selected tag is saved in the user defaults (by binding the selected tag in the Bindings inspector). Now instead of saving an integer I would like to save a string value (for the simple reason that it makes the user defaults more "readable" and failsafe), but unfortunately didn't find a way to bind a popup button's selected identifier. Is there a solution to this problem?
The bindings for NSPopupButton can be a little confusing. The various Content* bindings are used to provide the button with its list of possible selections. Content itself is used to provide a list of objects represented by the items in the pop up button. Content Values is used to provide the actual values displayed in the pop up button. For example, Content might be bound to an array of model objects, while Content Values is bound to a specific key path on those objects, for example name, because you want to display the value of the name property of each item in the popup button itself.
Similarly, the bindings for selection correspond to this system. Selected Object means that when a given item is selected, the underlying full object from the Content array will be selected/set on the bound property, not just the simple displayed string (or number, etc.) value. On the other hand, Selected Value will indeed bind just the displayed value.
Taken together, and in your case, where you're not using the content bindings, this means that you have two options:
Bind the Selected Value to user defaults.
Create an underlying model class with both an identifier property and a name (or whatever you want to call it) property. Bind the Content binding to an array of those objects, and the Content Values binding to thatArray.name.
Option 1
This option is much simpler. Just set up the selected values binding and you're done. It has the major disadvantage that the actual displayed string is the thing being stored in user defaults. This means that if you change the wording of an item, that previously stored selection won't be restored, even if it corresponds directly to the newly-worded item. More importantly, it's not a good idea to make localized -- or potentially localized -- strings semantically important.
Option 2
This takes a little more work (and code), but it would solve your problem in a robust, "correct" way. For example:
#objcMembers class Option: NSObject {
dynamic var name: String
dynamic var identifier: String
init(name: String, identifier: String) {
self.name = name
self.identifier = identifier
}
}
class ViewController: UIViewController {
#objc dynamic var optionsForPopup = [Option(name: "Item A", identifier: "id 1"),
Option(name: "Item B", identifier: "id 2"),
Option(name: "Item C", identifier: "id 3")]
}
Bind:
Content to ViewController - optionsForPopup.
Content Values to ViewController - optionsForPopup.name.
Selected Value to Shared User Defaults Controller - Controller Key: values, Model Key Path: WhateverUserDefaultsKeyYouWant.
Example
I've created an example project that implements option 2 here: https://github.com/armadsen/PopupDemo
Related
I have a view-based NSOutlineView with a dataSource/delegate model instead of binding to a tree controller (I want control over the insert/update animations).
I'm replacing an item in my model and would like to update that specific row in the outline view without having to call reloadData().
I cannot get this to work. Either the item does not update at all or the item's expanded state doesn't update. There seems to be some caching being done inside of NSOutlineView according to this, but even with these suggestions, I could not get it to work.
What I have:
(1) The outline view represents a folder structure
(2) At first, there is a singe file:
(3) The file is then replaced with a folder item:
// Model update
let oldFileItem = rootItem.children.first!
rootItem.children.remove(at: 0)
rootItem.children.append(Item(title: "Folder", children:[], isExpandable:true))
Expected result:
Actual result (reloadItem):
outlineView.reloadItem(oldFileItem) // I kept a reference
Icon and title reloaded, but note that the expansion triangle is missing.
I can somewhat understand that reloadItem() might not work in this case, because the old item is not part of the data model anymore. Strangely enough, the item's title and icon update, but not the expansion state.
Actual result (reloadData(forRowIndexes:columnIndexes:):
outlineView.reloadData(forRowIndexes: IndexSet(integer:0), columnIndexes: IndexSet(integer:0))
No effect whatsoever. This is the one that I would have expected to work.
Actual result (remove/insert):
outlineView.removeItems(at: IndexSet(integer:0), inParent: rootItem, withAnimation: [])
outlineView.insertItems(at: IndexSet(integer:0), inParent: rootItem, withAnimation: [])
No effect whatsoever.
The docs say about removeItems(): "The method does nothing if parent is not expanded." and isExpanded does indeed return false for the root node, although its children are visible. Is this special behavior for items that are direct children of the root node? What am I missing here?
For reference, my data model:
class Item:NSObject {
var title:String
var children:[Item]
var isExpandable:Bool
init(title:String, children:[Item], isExpandable:Bool) {
self.title = title
self.children = children
self.isExpandable = isExpandable
}
}
For reference:
It turned out to be an issue with how I used the API. NSOutlineView.removeItems/insertItems expect nil for the inParent parameter for the root item. I was handing in the actual root item. Using nil instead of the root item solved the problem.
I am trying to create an outlineview in a MacOS app that has mutliple levels that are summaries for a set of data held in SQLite3. I have an outlineview working with a treecontroller with a very simple NSMutuableDictionary based on a model class.
import Cocoa
class Summary: NSObject {
#objc dynamic var name: String
#objc dynamic var trades: Int
#objc dynamic var avgPL: Double
#objc dynamic var pandl: Double
#objc dynamic var parent: String
#objc dynamic var isLeaf: Bool
#objc dynamic var childCount: Int
#objc dynamic var children: [Summary] = []
init(name: String, trades: Int, avgPL: Double, pandl: Double, parent: String, isLeaf: Bool,childCount: Int) {
self.name = name
self.trades = trades
self.avgPL = avgPL
self.pandl = pandl
self.parent = parent
self.isLeaf = isLeaf
self.childCount = childCount
}
#objc func add(child: Summary) {
children.append(child)
}
}
My simple example data is:
let root: [String : Any] = ["name": "Overall","trades":5,"avgPL":200,"pandl":500,"parent":"","isLeaf": false,"childCount": 2 ]
let dict: NSMutableDictionary = NSMutableDictionary(dictionary: root)
let l2a = Summary(name: "L2a", trades: 3, avgPL: 100, pandl: 300, parent: "L1",isLeaf: true,childCount: 0)
let l2b = Summary(name: "L2b", trades: 2, avgPL: 100, pandl: 200, parent: "L1",isLeaf: true,childCount: 0)
dict.setObject([l2a,l2b], forKey: "children" as NSCopying)
I pass the dictionary to the treeController:
treeController.addObject(dict)
And that works nicely giving me a collapsible outline:
But I have no idea how to add more levels or children to the children. I want to have up to four levels deep in the outline. I have all the SQL summaries working and I have tried so many variations of populating arrays and trying to create a dictionary with the data to no avail. I have children and childCount and isLeaf set on everything but treecontroller does not like the array complaining that isLeaf is not KVO compliant. My data in an array looks like this (not all of the data but enough to see what I'm doing) The main level and all of the subsequent children are all based on the Summary model class above. Can I simply convert this array to a dictionary? Or, can I make it KVO compliant by adding keys to the model class or something? I have all of the 4 levels in separate arrays I use to build the resultant array if that is useful :
I should add that I have an NSObject defined as an NSMutableArray and its content tied to the treeController. My treeController is bound to each variable in the model class and at the top level has:
If I pass the array I have built to the treeController I get the following error:
Failed to set (contentViewController) user defined inspected property on (NSWindow): [<_TtGCs23_ContiguousArrayStorageC11outlinetest7Summary_ 0x604000445160> addObserver:forKeyPath:options:context:] is not supported. Key path: isLeaf
After building out my NSOutlineView without an NSTreeController and getting everything working I still wanted to get back to this and implement the treeController in order to take advantage of the sorting mechanism it provides. And I did find as per my last comment that I did have something wrong in InterfaceBuilder that was causing it to complain about KVO compliance. I had everything wired correctly except for the Content Array binding on the treeController. Here I bound it to my ViewController and added my data array reportSummary to the Model Key Path.
I also no longer needed to manually add my data array to the treeController using treeController.addObject(reportSummary). Once this was working I was then able to implement sorting and everything is working well. I should point out two things.
Setup of sorting on the TreeController is slightly different than on an ArrayController tied to a TableView. With the tableview it was sufficient to specify which columns are sortable in the identity inspector in IB. But in the outlineView scenario I also needed to setup bindings in IB to the treeController and change the Controller Key from arrangedObjects to sortDescriptors.
While testing my tree controlled outlineview I ran into a problem when I double-clicked on a summary row. I had implemented Double Action on the outlineView in IB in order to control the expanding and collapsing of summary sections. Note that I read about doing this in a thread here and someone mentioned that you would need to maintain multiple arrays and track indexes because once a row is collapsed or expanded that changes the row number of all the subsequent rows. But I figured out that the solution is simply to iterate through rows in reverse order and expand or collapse them working back up the tree starting from outlineView.numberOfRows-1. This works well and along with Double Action (clicking) to expand and collapse I also added an NSSlider which tracks to the expansion level and lets me collapse all the lowest levels moving back up the tree instead of clicking all of the little arrows on each row. This broke when I implemented the treeController. I received an error
Could not cast value of type 'NSKVONotifying_NSTreeControllerTreeNode'
This line of code was the problem
let summary = reportOutline.item(atRow: x) as! Summary
I had to change this to
let node = reportOutline.item(atRow: x) as! NSTreeNode
let summary = node.representedObject as! Summary
And that is it. Working beautifully now.
I have a modal window that contains a NSComboBox. "Uses Data Source" is set to turn and the combobox correctly lists the entries. The view controller is the delegate for the data source. When I call the modal window to update an existing object I would like the combobox to display the selected entry if there is one. How do I do this?
I've tried to access the entries in viewWillAppear. I get and error saying there are no entries. The various print statements I have in the code indicate that the values aren't loaded until the drop down arrow is clicked. The two function I'm supplying as the delegate are:
func numberOfItems(in comboBox: NSComboBox) -> Int
func comboBox(_ comboBox: NSComboBox, objectValueForItemAt index: Int) -> Any?
Would it be possible to set the selected entry in the second function above?
As NSComboBox is a subclass of NSTextField you should be able to set its text by setting the stringValue property of your comboBox.
self.comboBox.stringValue = "Hello World"
I've a really simple UI with a single NSPopUpButton. Its selectedIndex is bound to an int value ViewController.self.my_selection. As soon as I change the selection from the UI (i.e. a select the third item of the NSPopUpButton) I see that my_selection value changes. So far so good, what I'm trying to obtain is the opposed direction though. I want to change the my_selection value programmatically and see the NSPopUpButton selecting the item a the index that I've defined in my_selection. I erroneously supposed that behaviour was the default behaviour for bindings...
This is what I'm obtaining now:
NSPoPUpButton ---> select item at index 2 ----> my_selection becomes equal to 2
This is what I want to achieve (keeping also the previous behaviour)
my_selection ---> set value to 3----> NSPoPUpButton selected index = 3
Without a bit more info (see my comment) it's hard to see exactly what you're doing wrong. Here's how I got it working: First, create a simple class...
// Create a simple class
class Beatle: NSObject {
convenience init(name: String) {
self.init()
self.name = name
}
dynamic var name: String?
}
Then, in the AppDelegate I created a four-item array called beatles:
dynamic var beatles: [Beatle]?
func applicationDidFinishLaunching(aNotification: NSNotification) {
beatles = [Beatle(name: "John"),
Beatle(name: "Paul"),
Beatle(name: "Ringo"),
Beatle(name: "George")]
}
In Interface Builder I set things up so that this array provides the pop-up with its content:
This class also has a selectedIndex property that is bound to the pop-up button's selectedIndex binding - this property provides read-write access to the pop-up button's selection:
// Set the pop-up selection by changing the value of this variable.
// (Make sure you mark it as dynamic.)
dynamic var selectedIndex: Int = 0
I have a custom view with two text subviews, arranged, not that it matters, as per this amazing ASCII art:
/--------\
| lblOne |
| lblTwo |
\--------/
On my controller, I have a property of type Thingy:
class AwesomeController: NSViewController {
var thingy: Thingy! = nil
}
A Thingy has two properties of interest:
class Thingy: NSObject {
var one: String
var two: String
}
I would like to set up a binding between lblOne's string value and thingy.one, and lblTwo's string value and thingy.two, going through a custom view class if necessary.
When thingy is changed, obviously the two text fields should also change. (In other words, it should behave normally for a cocoa binding.)
I think it's probably a combination of learning Swift and my unfamiliarity with storyboards on OS X (last time I did cocoa development, it was still xibs), but I can't work out how to link the damn thing up.
Getting bindings to work in Swift requires two additional steps:
All the vars must be marked with dynamic, eg:
dynamic var one: String
You have to recompile the project (with cmd+B, not just in the background) in order for the var to appear as an option in IB