I'm having difficulty binding a NSPopUpButton to an NSArrayController.
The array controller manages an array (plants) of the the class Plant, which has a property named commonName which should be listed in the button. I've searched for several days and I'm unable to discover why this isn't working. I'm able to get the button to display the elements of an array of strings but not with the plants array. When the program runs, the button has no elements and doesn't react to clicking.
I've included a screenshot of the attributes and bindings but here's a description:
ArrayController
Attributes: Mode = Class; Class Name = TestDB.Plant (TestDB is the
name of the project)
Binding: Bound to View Controller; Model Key
Path = plants
Button Bindings
Content: Bound to Array Controller; Controller Key = arrangedObjects
Content Values: Bound to Array Controller; Controller Key =
arrangedObjects; Model Key Path = objectValue.commonName
Here is the code from the ViewController:
class ViewController: NSViewController {
#IBInspectable var plants: [Plant] = []
#IBOutlet weak var plantPopUp: NSPopUpButton!
override func viewDidLoad() {
super.viewDidLoad()
//the real list will be pulled from a database, but I'm using
//this to test binding the button
plants = [Plant(commonName: "Asparagus", scientificName: "Asparagus officials"),
Plant(commonName: "Beet", scientificName: "Beta vulgaris")]
//to redraw the button?? Doesn't change anything with or without
plantPopUp.needsLayout.true
}
}
This is the code for the class Plant:
#objc class Plant: NSObject {
#objc dynamic var commonName: String
#objc dynamic var scientificName: String
init(commonName: String, scientificName: String) {
self.commonName = commonName
self.scientificName = scientificName
}
}
Here are screenshots of the attributes and bindings of the NSArrayController and the NSPopupButton. Very grateful for any help.
Two changes:
You have to make plants also KVC compliant
#IBInspectable #objc dynamic var plants: [Plant] = []
Button Bindings - Content Values: Bound to ... Model Key Path = commonName (delete objectValue.)
Related
I have a manager class for my data which is configured by two properties, one to set to a category and another to select items which correspond with that category. Based on that it will expose the relevant pieces of data. I am using a couple of different forms or making those selections, including a pair of IndexSets.
My problem is that I would also like to be able to save the selected items for each category, so that whenever the category is changed the items previously selected for it are restored. This is easy to achieve when accessed programmatically, but using bindings to allow a view in a macOS app to be able to provide that configuration unfortunately does not work properly
Changing the category causes the object bound to its selection to empty or 'preserve' the selected items before the category is actually updated. So the actual selection gets overwritten with, with noway I can see to tell the difference between this behaviour and a user action.
Here are the test code I have used for experimenting, with viewDidLoad generating some random test data to roughly mimic the structure o the real class. This does not attempt to save or restore the selection, but simply shows the overwriting behaviour.
class Thing: NSObject {
#objc dynamic var name: String
required init(name: String) {
self.name = name
}
}
class Stuff: NSObject {
#objc dynamic var name: String
#objc dynamic var things: [Thing]
required init(name: String, things: [Thing]) {
self.name = name
self.things = things
}
}
class StuffManager: NSObject {
#objc dynamic var stuff = [Stuff]()
#objc dynamic var stuffIndex = IndexSet() {
didSet {
print("STUFF: ", Array(stuffIndex))
}
}
#objc dynamic var things = [Thing]()
#objc dynamic var thingsIndex = IndexSet() {
didSet {
print("THING: ", Array(thingsIndex))
}
}
}
class ViewController: NSViewController {
#objc dynamic var stuffManager = StuffManager()
override func viewDidLoad() {
super.viewDidLoad()
(1...10).forEach { stuffManager.things.append(Thing(name: "Thing \($0)")) }
(1...9).forEach {
let randomThings = Array(stuffManager.things.shuffled()[0...Int.random(in: 0..<10)])
stuffManager.stuff.append(Stuff(name: "Collection \($0)", things: randomThings))
}
stuffManager.stuff.append(Stuff(name: "Collection 10", things: []))
}
}
In Interface Builder I have a view containing an NSPopButton to select the Stuff, a multiple selection NSTableView to select the Things, and a pair of NSArrayControllers for each. The bindings are:
Stuff Array Controller
Content Array:
Binding to: ViewController, Model Key Path: stuffManager.stuff
Selection Indexes:
Binding to: ViewController, Model Key Path: stuffManager.stuffIndex
Things Array Controller
Content Array:
Binding to: Stuff Array Controller, Controller Key: Selection, Model Key Path: things
Selection Indexes:
Binding to: ViewController, Model Key Path: stuffManager.thingIndex
The two interface objects are bound to these controllers in the standard way, the Content to the arrangedObjects and the Selection Indexes to the selectionIndexes of their respective array controller.
What this test code shows is that when the value in the popup button is changed the THING debug line appears before the STUFF debug line, that is it changes the selection of Things before it changes the Stuff. So any code in the property observer on stuffManager.things to save the new selection will save this change before being aware that the Stuff has changed.
Obviously this behaviour is to avoid the selection being made incorrect by the change to the content, or worse selecting out of bounds if the new content is shorter. But is there any way to detect when this is happening, rather than a user changing the selection? Or a way to override it to gain manual control over the process rather than having to accept the default behaviour of 'Preserve Selection' or the selection being cancelled if that option is disabled?
And what makes it more awkward is if this behaviour only occurs when the selection would change. If the selected Things exist for the new Stuff, or if nothing is selected, then nothing happens to trigger the property observer. Again this is understandable, but it prevents being able to cache the change and then only save the previous one if the Stuff has not changed.
I did wonder if using a separate IndexSet for each Stuff would avoid this problem, because then there would be no need for the NSTableView to manage the selection. I do not like the idea of keeping an IndexSet in the model but would accept it if it worked. But it does not. Again understandable, because the table view has no idea the Selection Indexes binding will be changed. Unless I am missing something?
But I tested this by updating the Stuff class to include the following:
#objc dynamic var selected = IndexSet() {
didSet {
print("THING: ", Array(selected))
}
}
Then changing the Selection Indexes binding of the Things Array Controller to:
Binding to: Stuff Array Controller, Controller Key: selection, Model Key Path: selected
Is what I am trying to achieve impossible? I would not have thought it that strange a thing to want to do, to save and restore a selection, but it seems impossible with bindings.
The only solution I can see is to forgo the master-detail style pattern and instead just maintain a separate [Thing] property in my data manager class, bind the Things Array Controller to this (or even just bind the table directly to the property), then whenever the popup button changes update the new property to match the stuff object.
Something like this in the StuffManager, with the table content bound to availableThings:
#objc dynamic var stuffIndex = IndexSet() {
didSet {
print("STUFF: ", Array(stuffIndex))
availableThings = stuff[stuffIndex.first!].things
}
}
#objc dynamic var availableThings = [Thing]()
It appears there is no way to prevent the NSTableView behaviour of automatically resetting its selection when the content changes. Nor any way to detect when this is happening, as it updates this before updating the selection on the NSPopupButton having changed. So here is how I have written the StuffManager class, adding a property for binding to the tableview so I can control the content changing:
class StuffManager: NSObject {
let defaults: UserDefaults = .standard
var canSaveThingsIndex = true
#objc dynamic var stuff = [Stuff]()
#objc dynamic var stuffIndex = IndexSet() {
didSet {
canSaveThingsIndex = false
if stuffIndex.count > 0 {
availableThings = stuff[stuffIndex.first!].things
let thing = stuff[stuffIndex.first!].name
if let items = defaults.object(forKey: thing) as? [Int] {
thingsIndex = IndexSet(items)
} else if availableThings.count > 0 {
thingsIndex = IndexSet(0..<availableThings.count)
} else {
thingsIndex.removeAll()
}
} else {
availableThings.removeAll()
thingsIndex.removeAll()
}
canSaveThingsIndex = true
}
}
#objc dynamic var things = [Thing]()
#objc dynamic var availableThings = [Thing]()
#objc dynamic var thingsIndex = IndexSet() {
didSet {
if canSaveThingsIndex && stuffIndex.count > 0 {
let thing = stuff[stuffIndex.first!].name
defaults.set(Array(thingsIndex), forKey: thing)
}
}
}
}
The Things Array Controller is now bound as:
Content Array:
Binding to: ViewController, Model Key Path: stuffManager.availableThings
Selection Indexes:
Binding to: ViewController, Model Key Path: stuffManager.thingsIndex
Though without being able to use the master-detail benefits of an NSArrayController they are not needed. Both the NSPopupButton and NSTableView can be bound directly to the StuffManager. And this allows the NSPopupButton's Selected Index can be bound to an Int int he Stuff Manager rather than needing to use an IndexSet despite multiple selections being impossible.
The main feature of the workaround is that because I am manually changing the content I can use the canSaveThingsIndex flag before changing the NSTableView content. So whenever its natural behaviour triggers the thingsIndex property observer, this can be ignored to prevent it overwriting the user's selection. It also avoids the unnecessary saving of a selection immediately after being restored.
I am trying to create a macOS project using Xcode 9.4.1 with Swift 4.2. I am using a Document-Based Application since I am entering data. The project has a class that includes an array as a property along with other properties. This class will be used to enter data that will need to persist and I'm trying to use IB and bindings to get this to work.
Here is the class (The actual class has much more String, Bool and Array properties):
class MyClass: NSObject {
#objc dynamic var property1 = ""
#objc dynamic var property2 = true
#objc dynamic var property3 = true
#objc dynamic var myClass2s: [myClass2] = []
}
class myClass2: NSObject {
#objc dynamic var property4 = ""
#objc dynamic var property5 = ""
}
I can get a tableView with an arrayController to work for the String and Bool properties. The difficulty for me comes in when I need to have another tableView for the MyClass2 data to be entered, which requires another arrayController. I can't seem to figure out how to bind it all together.
EDIT 9/30
I've tried binding the details tableView's Table Content to MyClassArrayController, Controller Key: selection, Model Key Path: myClass2s. I have a second Array Controller for MyClass2 so I can add/remove data to MyClass2. When I run the project, I don't get any errors, however, I can't add data to MyClass2 objects with its table View content bound to the array myClass2s.
So, in addition to what I tried above, I bound MyClass2ArrayController's Content Array to MyClassArrayController, selection, myClass2s and so far it appears to be working now.
I am trying to set up an NSOutlineView using a TreeController.
I have the following class for each node in the tree:
#objc class EdLevel: NSObject{
#objc dynamic var isLeaf: Bool { return children.count == 0}
#objc dynamic var childCount: Int { return children.count}
#objc dynamic var children: [EdLevel] = []
#objc dynamic var name: String
#objc init(name n: String){
name = n
}
}
In my Xcode story board I have done the following settings:
I currently have the following property in my view controller (called Admin in IB)
#objc dynamic var eddingtonNumberLevels: [EdLevel] = []
When I run this I get an exception at my AppDelegate with no information about where it failed. I get the same thing if I remove the “Count” and “Leaf” Key paths (which I understand to be optional anyway as they can be derived from “Children”]. When I remove the Children key path it all runs (with no content of course) but I get “childrenKeyPath cannot be nil. To eliminate this log message, set the childrenKeyPath attribute in Interface Builder”
I’ve searched for answers but they all just say set the childrenKeyPath with explaining what form that property should take. I’ve tried changing children to be an NSArray but that didn’t help.
Any suggestions on what the problem ?
I am trying to create a global variable from my view controller (inside the class but outside the functions) as follows:
var modelData: CustomTabBarController.model // THE ERROR IS HERE
This is how that class is defined:
CustomTabBarController.swift:
import UIKit
// This class holds the data for my model.
class ModelData {
var name = "Fred"
var age = 50
}
class CustomTabBarController: UITabBarController {
// Instantiate the one copy of the model data that will be accessed
// by all of the tabs.
var model = ModelData()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
}
However I am getting the following error:
"'model' is not a member type of "CustomTabBarController"
How do I declare it so that I can access model? Thanks.
Update #1
Sorry I forgot to mention this:
I need the model data to be the SAME in every tab of the tabbar. For example if I change the age to 51 in the first tab, the second tabbar should retrieve 51. Which would be the correct method above to use it this way?
Update #2
I am able to create the variable inside a function with dean's suggestion:
func setupModelData()
{
var modelData = (self.tabBarController as! CustomTabBarController).model
}
However this does not work, since I need to access the modelData from other functions. When I attempt to move this line outside of the function as follows:
import UIKit
class FirstViewController: UIViewController, UITableViewDelegate, UITableViewDataSource
{
var modelData = (self.tabBarController as! CustomTabBarController).model
...
I receive the error:
Value of type '(NSObject) -> () -> FirstViewController' has no member 'tabBarController'
I ended up following holex's suggestion of creating a shared class (singleton):
import UIKit
class ModelData: NSObject {
static let shared: ModelData = ModelData()
var name = "Fred"
var age = 50
}
Writing in first view: Set age to 51:
ModelData.shared.age = 51
Reading in second view: Get age of 51
let age = ModelData.shared.age
I'm not sure whether you truly want a global variable (i.e. a single instance of ModelData shared between all your view controllers) or an instance variable which is public, so I'll try to answer both :)
1) global model
This line attempts to get the model property from the CustomerTabBarController class - i.e. if you made multiple tab bar controllers they would all use the same model.
var modelData: CustomTabBarController.model
If this is what you want, then you need to change this line to include the static keyword.
static var model = ModelData()
However, this almost certainly isn't what you're after.
2) shared instance variable
This means that the model is part of each instance of CustomTabBarController. Here, you would need to change the line which is throwing the error to be something like this:
var modelData: myCustomTabBarController.model
Without knowing more about your architecture, I can't help you get hold of your tab bar controller instance, but something like this might work (inside other view controllers):
var modelData = (self.tabBarController as! CustomTabBarController).model
model is an instance variable.
Either create an instance of CustomTabBarController
var modelData = CustomTabBarController().model
Or declare model as class variable
static var model = ModelData()
...
var modelData = CustomTabBarController.model
However to use ModelData as a single global variable with the two members, use a struct rather than a class and declare the members as static.
struct ModelData {
static var name = "Fred"
static var age = 50
}
You can access the name from everywhere for example
let name = ModelData.name
and there is no need to create an extra variable in another class.
An – instance based – alternative is a singleton
struct ModelData {
static let shared = ModelData()
var name = "Fred"
var age = 50
}
and use it
let name = ModelData.shared.name
I'm banging my head against the wall trying to figure this out. I have a very simple app with a class Person, an NSTableView and an NSArrayController "PersonController".
Person:
class Person: NSObject {
var firstName = ""
var lastName = ""
}
PersonController:
Added outlet to ViewController
Attributes Inspector > Object Controller > Class Name = Person
TableView:
Attributes Inspector > Table View > Content Mode = Cell based
1st Table Column bound to Person Controller with Controller Key: arrangedObjects and Model key path: firstName
2nd Table Column bound to Person Controller with Controller Key: arrangedObjects and Model key path: lastName
ViewController:
class ViewController: NSViewController {
#IBOutlet var personController: NSArrayController!
override func viewDidLoad() {
super.viewDidLoad()
let person = Person()
var people = [Person]()
person.firstName = "John"
person.lastName = "Snow"
people.append(person)
person.firstName = "Kate"
person.lastName = "Dawson"
people.append(person)
person.firstName = "Tom"
person.lastName = "Anderson"
people.append(person)
personController.addObjects(people)
}
override var representedObject: AnyObject? {
didSet {}
}
}
Output:
What am I doing wrong here? I've read many SO posts and a ton of tutorials etc and from what I can tell I'm doing it correctly but I can't for the life of me get this to work.
You only have one Person object. When you append it to the array, that doesn't make a copy. The array just contains a reference to that one object. You're appending the same object three times, so the array contains three references to that one object.
Each time you change that one Person's firstName and lastName, you're changing that one object's properties. So, at each index of the array, the table finds that one object with the last values set for its name properties.
You need to create three separate Person objects.
As a separate matter, you need to mark the properties of Person as dynamic if you want them to be Key-Value-Observing-compliant and thus Bindings-compatible.