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.
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'm learning Swift and I'm wondering how can I create a data structure with multiple values and pass descriptions values from UITableViewController to another viewController? I have tried like this
struct faculty {
var name = String()
var descriptions = (String)[]
}
let faculties = [name: "Faculties", description: ["Study1", "Study2"]]
I have successfully managed to list an array ["Test1", "Test2"] in tableView.
There are a couple of issues
An empty string array is [String]().
description is not equal to descriptions.
An instance must be created with Type(parameter1:parameter2:).
And structs are supposed to be named with starting capital letter.
struct Faculty {
var name = String()
var descriptions = [String]()
}
let faculties = [Faculty(name: "Faculties", descriptions: ["Study1", "Study2"])]
However default values are not needed. This is also valid
struct Faculty {
let name : String
var descriptions : [String]
}
Say I have an array of object 'YearGroup' called yeargroups. Each YearGroup has an array of object 'Student' called students.
In a function inside of a Student object, it is useful for me to know the index of the YearGroup it is in. How would I find this?
The below code is the example of this. I've made a similar function within YearGroup that finds it's own index in yeargroups, but how would I write a similar function within a Student?
class YearGroup {
var id = UUID()
var students = [Student]()
var myIndex = 0
func findMyIndex() {
myIndex = yeargroups.firstIndex(where: {$0.id == self.id})!
}
}
class Student {
let id = UUID()
var name:String
var age:Int
var myYearGroupIndex = 0
init(n:String, a:Int) {
name = n
age = a
}
func findMyYearGroupIndex() {
myYearGroupIndex = ????????
}
}
var yeargroups = [YearGroup]()
This code doesn't actually add any YearGroup or Student objects but I hope you get the idea.
Thanks!
As your code is right now a Student could be in any number of YearGroups. You could avoid potential confusion there by making the YearGroup a student is a member of a property of Student.
With that in place you can have the findMyYearGroupIndex() do a search like the one in YearGroup::findMyIndex().
If you really don’t want YearGroup to be a member of student you could pass it as an argument to findMyYearGroupIndex(groups: [YearGroups]).
Looking at this I’m not sure if YearGroups and the students make a 2 dimensional array. If so you need to store the index either as a pair (Int, Int) or an array with two elements [Int].
Sorry I don’t have code blocks in here. I can’t seem to make them on my phone.
I have a Swift Realm database that I’m attempting to find a specific record which will occupy a number of Labels in a UIViewController - no Tableview. In essence, I want to search the database for a record based on a String variable consisting of a date and time. The string format looks like this “Feb19,21-15:47” but changes with each new record added - hence why I need to use a string variable as a search parameter.
Once the record is found I want to then grab the entire record associated with the search string and parse out each field to fill the five Labels on the VC. I’ve been trying for hours to get this to work but I’m just not getting the result I need.
My questions are:
How do I format the search parameter to find the record using tempPhotoTitle?
Once the search finds the object property (tempPhotoTitle) in the database what code needs to be employed to grab all the associated properties in the same record/row so I can bind each property to its associated Label in the VC.
A couple notes: I did employ an auto updating primary key in each record named ‘id’. When using a Tableview I can access each record by using indexPath.row but since I’m not using a TV this isn’t available. The tempPhotoTitle string value is fed from another VC via a segue. Also only one record in the DB will have the search value. Here is some abbreviated code to provide the gist of my issue. The search doesn’t work (parsing issue) and as a result I can’t test the remaining code. I sure would appreciate some assistance on this. Many thanks and frustrated Roger
import UIKit
import RealmSwift
class Snapshot: Object {
#objc dynamic var id = 0
#objc dynamic var date: String = ""
#objc dynamic var cTime: String = ""
#objc dynamic var airTemp: String = ""
#objc dynamic var humidity: String = ""
#objc dynamic var photoTitle: String = ""
override class func primaryKey() -> String {
return "id"
}
convenience init( ….)
}
class RecordsVC: UIViewController {
var tempPhotoTitle: String = ""
var tempImage = UIImage()
#IBOutlet weak var eDateHolder: UILabel!
#IBOutlet weak var eTimeHolder: UILabel!
#IBOutlet weak var eAirTempHolder: UILabel!
#IBOutlet weak var eHumidityHolder: UILabel!
var editSnapShotItems: Results<Snapshot>?
override func viewDidLoad() {
super.viewDidLoad()
queryRecords()
}}
func queryRecords() {
let realm = try! Realm()
let allRecords = realm.objects(editSnapShotItems.self)
let recordResult = allRecords.filter("photoTitle CONTAINS[cd] %#", tempPhotoTitle)
let recordResults = allRecords.filter(tempPhotoTitle)
for record in recordResults {
eDateHolder.text = record.date
eTimeHolder.text = record.cTime
eAirTempHolder.text = record.airTemp
eHumidityHolder.text = record.humidity
}}}
Here's a simplified version of your object for this answer
class Snapshot: Object {
#objc dynamic var date: String = ""
}
if you create an object
let snap = Snapshot()
snap.date = "Feb19,21-15:47"
and then store it in Realm
let realm = try! Realm()
realm.write {
realm.add(snap)
}
and then you want to find that later
let snapResults = realm.object(Snapshot.self).filter("date == %#", "Feb19,21-15:47")
for snap in snapResults {
print(snap.date)
}
the console output will be
Feb19,21-15:47
But...
There are a number of issues with storing dates in that format. For example, what if you want to sort three dates Jan20,21-15:57, Jan21,21-15:57, Feb19,21-15:57. Logically they are in order but because they are strings, they will sort with Feb19 first (F is before J in the alphabet).
There are a number of solutions: Realm fully supports NSDate so you can just store them as an actual date or if you really want to use strings, store them in a sortable format
202102191547
would be Feb 19th 2021 at 15:47, making sure single digits are padded with a 0. This allows them to be ordered, filtered etc correctly
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.)