How to trigger function by using check boxes in nstableview, in swift - swift

I have nstableview in mac application. One column has names, second - numbers and third has checkboxes. When user make checkbox ON this should trigger function according to the name in first column in the same row. To have the name from first column (for triggering function) I need to know row index, and I use function rowForView(_ view: NSView). Question is what I should use as parameter (_ view: NSView) in this function. Below is the code. I am not sure if I am going in right direction to get what I have described above.
Related question is how I can get array of names from first column of table. I need this because if I will sort table according to the name then I cannot use my array, which used as data to fill the nstableview, I need array of sorted names to correctly trigger function.
#IBOutlet weak var myCheckbox: NSButton!
#IBAction func checkboxToShowRestrctaseSites(sender: AnyObject) {
if myCheckbox.state == NSOnState
{
let objectOfPresentRestrictase = RestrictasesSorting(restrictase: "", dna: inputDnaFromUser.string!)
var tableOfNumberedRestrictase = objectOfPresentRestrictase.makeListOfPresentRestrictase()
var listOfAllRestrictaseWithSites = objectOfPresentRestrictase.searchForRestrictionSiteInList().1
var row = myTable.rowForView(_ view: NSView)
var name = Array(tableOfNumberedRestrictase.keys)[row]
var site = listOfAllRestrictaseWithSites[name]
UPDATE
I have found that triggering function start to work if I remove if myCheckbox.state == NSOnState, and when I used sender as NSView as parameter for function rowForView. To control the ON and Off state I can also use sender in if condition. Code below.
#IBAction func checkboxToShowRestrctaseSites(sender: AnyObject){
if sender.state == NSOnState
{
let objectOfPresentRestrictase = RestrictasesSorting(restrictase: "", dna: inputDnaFromUser.string!)
var tableOfNumberedRestrictase = objectOfPresentRestrictase.makeListOfPresentRestrictase()
var listOfAllRestrictaseWithSites = objectOfPresentRestrictase.searchForRestrictionSiteInList().1
var row = myTable.rowForView(sender as! NSView)
var name = Array(tableOfNumberedRestrictase.keys)[row]
var site = listOfAllRestrictaseWithSites[name]

The documentation of rowForView:
Return Value
The index of the row corresponding to the view. Returns -1 if view is not an instance of NSTableRowView or a subview of an instance of NSTableRowView. In other words, if view is not in a table view, this method returns -1. (Note that this method may also return -1 when a row is being animated away, because view no longer has a valid row.).
Sounds like you can use the checkbox.
Your datasource does the sorting and has the data for the first column.

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.

Swift Realm filter issue

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

How to convert String to Int in Swift?

There has been same titles of this question but different situations.
In this case this is very simple but I can't find a same problem online.
So here's the code
class ViewController: UIViewController {
#IBOutlet weak var fldTotalUnits: UITextField!
var intTotalUnits:Int? = Int(fldTotalUnits)
The error here says "Cannot use instance member 'fldTotalUnits' within property initializer;..."
I tried replacing var with let, I tried NSString, I tried .toInt() but nothign worked... so how do I this?
String to Int conversion is not complicated. You simply do the conversion at the wrong place. You are trying to reference one member in the initialization of another member, that is not allowed. In this particular case simply because fldTotalUnits has the value nil when you would try to use it via Int(fldTotalUnits). When creating an instance of your class ViewController fldTotalUnits is set to nil and initialized with a useful value later. Therefore what you have to do in the first place is move the line into a separate method:
func doSomething() {
var intTotalUnits:Int? = Int(fldTotalUnits)
}
Now you will see that the compiler complains about there not being a suitable initializer because you have to access the text property of fldTotalUnits instead of using the actual textfield:
func doSomething() {
var intTotalUnits:Int? = Int(fldTotalUnits.text!)
}
Now you can think about moving the declaration of intTotalUnits to somewhere else, but setting its value has to happen in some method.
The code in your question is trying to create an Int from a UITextField, not a String. What you should say is something like…
var intTotalUnits:Int?
func updateTotalUnits()
guard let text = fldTotalUnits.text else { return }
intTotalUnits = Int(text)
}

Retrieve Values from Array with class

I have a custom class defined as such:
public class Location {
var name = String()
var address = String()
var place = String()
}
I'm then populating an Array using that class as follows:
var chosenLocation = [Location]()
var locationResult = Location()
//Code to parse data here//
for result in results! {
locationResult.name = result["name"] as String
locationResult.address = "Bogus Address"
locationResult.place = result["place"] as String
chosenLocation.append(locationResult)
}
This all seems to be working fine, but when I try to get the individual "name" value in cellForRowAtIndexPath I just get the last record over and over. I guess I just don't understand how to reference each entry since it's a class wrapped in an array. The code I believe is in question, and which is returning the same row over and over is:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell:UITableViewCell = UITableViewCell(style:UITableViewCellStyle.Default, reuseIdentifier:"cell")
var locationAnswer = Location()
locationAnswer = chosenLocation[indexPath.row
cell.textLabel?.text = locationAnswer.name
return cell
}
I believe it's getting appended to chosenLocation correctly, but since I don't know how to "unwrap" it , a println only shows me that I have the correct number of values and not what's in them.
Thanks a bunch for any help you can provide!
It looks like the bug is that just a single Location object is created and updated, so it contains the data from the very last update
Move the creation to be within the for loop...
// var locationResult = Location() <- Remove this
for result in results! {
var locationResult = Location() // <- Add it here
...
#Jawwad provided a solution to the problem.
Note that your code doesn't work because the item you are adding to the array is an instance of a reference type (class), so you are instantiating once, initializing at each iteration, then adding to the array - but what's added is just a copy of the reference to the instance, and not the instance itself.
Your code would work just fine if you turn the Location class into a struct. Being value types, structs are passed by value and not by reference, so the action of passing the same instance to the append method results in a copy of that instance to be created and passed.

How can we transfer data between classes when using swift programming language

With Objective-C we could transfer data to an array at an UIView class from ViewController using a method.
In an UIView class we were using something like this
float values[10];
-(void) getValue:(float) value index:(int) index {
values[index] = value
}
But when we try doing a similar thing with Swift such as
var values : [CGFloat] = [10]
func getValue (value:CGFloat, index:Int) {
values [index] = value
}
We are getting " fatal error: Array index out of range error " or if we use
var values : [CGFloat] = [10]
var index = 0
func getValue (value:CGFloat) {
values.append = value
++index
}
We are not getting error message but whenever we use setNeedsDisplay() array values are being set to initial values which is 10 for this example.
Till now we are unable to convert Objective-C UIView classes like that to Swift one.
First:
var values : [CGFloat] = [10]
That line says that values is a variable array of CGFloat values that currently holds a single value of 10. Therefore, only index 0 actually exists.
Also:
func getValue(value:CGFloat, index:Int) {
values [index] = value
}
Never mind the fact that you have put this on the UIView class, never mind that you have a method named "getValue" that actually sets a value...
EDIT:
I found a better solution:
var values = Array<Float>(count:10, repeatedValue: 0)
func getValue(value: Float, index: Int) {
values[index] = value
}
The above is the direct Swift equivalent to the Objective-C code you posted in your question.
Frankly, I think you should take a step back and find a better way to solve the problem you think this code is solving. As it stands it doesn't make a whole lot of sense.
var values : [CGFloat] = [10]
In swift, this will create an array of CGFloat with one element: 10.0, not 10 elements.
so values.count is one
Swift Collections
Yes, after studying some more Swift I found the answer to my question.
Using static variable in the UIView class is the answer.
Define variable like that in UIView class
struct StructToChange {
static var varToChange = 1
}
Write a function to change variable value like this in UIView class.
func getIt (newValue:Int) {
StructToChange.varToChange = newValue
}
Call the function in a controller class this way
let valueChanger = AnyUIViewClass()
valueChanger.getIt ( 10 )
This is it you changed the value of variable and you can use it as parameter in your " override func drawRect(rect: CGRect) { } "