Setting TreeController childrenKeyPath throwing error - swift

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 ?

Related

Can you save the selection of a master-detail bound NSTableView?

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.

'Cannot sort on key path, property does not exist' - RealmSwift

So I am getting this fatal error:
Terminating app due to uncaught exception 'RLMException', reason: 'Cannot sort on key path 'title': property 'Item.title' does not exist.'
I have a Class Category which contains a list of my Class Item, and the two have a Linking Objects relationship. I am using Realm, and I get the error upon clicking a Category cell in my tableview, which is supposed to bring me to a tableview of all the Items in the selectedCategory. Here is the function (the two variables are at the beginning of my file, I put them here so you can see):
var toDoItems : Results<Item>?
var selectedCategory : Category? {
didSet {
loadItems()
}
}
func loadItems() {
toDoItems = selectedCategory?.items.sorted(byKeyPath: "title", ascending: true)
tableView.reloadData()
}
Just to be clear, the selectedCategory gets set in my CategoryViewController through a tableView.indexPathForSelectedRow property, which is how it obtains its value in my ToDoListViewController, where the loadItems() function is. And here are my classes of Category and Item, (in their own files of course):
import Foundation
import RealmSwift
class Category: Object {
dynamic var name: String = ""
let items = List<Item>()
}
import Foundation
import RealmSwift
class Item: Object {
dynamic var title: String = ""
dynamic var done: Bool = false
dynamic var dateCreated: Double = 0.0
var parentCategory = LinkingObjects(fromType: Category.self, property: "items")
}
Why am I getting the error that the Item.title property does not exist? It clearly does. I have erased and rewritten the variable, restarted Xcode, etc, and the error is still there. Thanks in advance for any help!
Your Realm object is not constructed correctly. You either need to mark the individual properties as #objc
class Item: Object {
#objc dynamic var title: String = ""
or mark the whole object as #objmembers
#objcMembers class Item: Object {
dynamic var title: String = ""
I prerfer to specify each property as it gives more more control as some I don't always want all vars managed.
To answer the part below about why the app is still crashing, all I had to do was go to my simulator, and delete the app on the simulator, then restart it. Fixed!

binding NSPopupbutton to an array of classes

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.)

Trouble binding a data class that has numerous String, Bool and Array properties. Using Xcode 9.4.1 with Swift 4.2 for macOS

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.

Swift ObjectMapper not calling didSet

I'm using ObjectMapper to populate the properties of my Realm object from some JSON. But the didSet doesn't seem to be called whenever the remotePath property is changed by the Mapper.
Here's a trimmed down version of my class (i've just removed a bunch of properties to keep i concise)
class ImageFile: Object, Mappable {
dynamic var id = 0
dynamic var filename = ""
dynamic var remotepath = "" {
didSet {
self.filename = self.remotepath.isEmpty ? "x" : "z"
}
}
required convenience init?(map: Map) {
self.init()
}
override static func primaryKey() -> String? {
return "id"
}
func mapping(map: Map) {
remotepath <- map[“path"]
}
}
I'm invoking this like so:
let file = ImageFile()
file.id = json["fileId"].int
// Map the properties
file.mapping(map: Map(mappingType: .fromJSON, JSON: json.dictionaryObject ?? [:]))
I've tried putting a breakpoint inside the didSet as well as a print just to see if it's firing and it isn't/ But if i check the database, I do see the correct value inside the remotepath property, so it's definitely being filled in.
I know that the didSet wont fire during init, but i'm mapping after the init so it should fire? I've checked github issues on ObjectMapper and people have used didSet with ObjectMapper, so i'm obviously missing something...
Any help is greatly appreciated :)
Notes:
All of the properties of this object get filled in, except filename one which is supposed to be set inside the didSet callback.
There's a reason i chose to not map the ID, just ignore that, provided just for clarity.
I've used "x" and "z" as the filename just to test if it's being triggered, just ignore the fact they make no sense, that's not the problem.
Property observers don't work on Realm objects. This is a known limitation of Realm due to the fact that objects exist in the Objective-C runtime (hence the dynamic modifier). See this GitHub issue for more details. You can use Realms built in notifications as a workaround for property observers.