NSTableView detect NSTableColumn for selected cell at start of cell edition - swift

I'm trying to programatically get get a a column.identifier for the cell that is being edited. I'm trying to get by registering my NSViewController for NSControlTextDidBeginEditingNotification and when I get the notification I track the data by mouse location:
var selectedRow = -1
var selectedColumn: NSTableColumn?
func editingStarted(notification: NSNotification) {
selectedRow = participantTable.rowAtPoint(participantTable.convertPoint(NSEvent.mouseLocation(), fromView: nil))
let columnIndex = participantTable.columnAtPoint(participantTable.convertPoint(NSEvent.mouseLocation(), fromView: nil))
selectedColumn = participantTable.tableColumns[columnIndex]
}
The problem I have is that the mouse location is giving me the wrong data, is there a way to get the mouse location based on the location of the table, or could there be a better way to get this information?
PS. My NSViewController is NSTableViewDelegate and NSTableViewDataSource, my NSTableView is View Based and connects to an ArrayController which updates correctly, and I could go to my Model object and detect changes in the willSet or didSet properties, but I need to detect when a change is being made by the user and this is why I need to detect the change before it happens on the NSTableView.

This question is 1 year old but I got the same issue today and fixed it. People helped me a lot here so I will contribute myself if someone found this thread.
Here is the solution :
1/ Add the NSTextFieldDelegate to your ViewController :
class ViewController: NSViewController, NSTableViewDelegate, NSTableViewDataSource, NSTextFieldDelegate {
2/ When a user wants to edit a cell, he had first to select the row. So we will detect that with this delegate function :
func tableViewSelectionDidChange(_ notification: Notification) {
let selectedRow = self.tableView.selectedRow
// If the user selected a row. (When no row is selected, the index is -1)
if (selectedRow > -1) {
let myCell = self.tableView.view(atColumn: self.tableView.column(withIdentifier: "myColumnIdentifier"), row: selectedRow, makeIfNecessary: true) as! NSTableCellView
// Get the textField to detect and add it the delegate
let textField = myCell.textField
textField?.delegate = self
}
}
3/ When the user will edit the cell, we can get the event (and the data) with 3 different functions. Pick the ones you need :
override func controlTextDidBeginEditing(_ obj: Notification) {
// Get the data when the user begin to write
}
override func controlTextDidEndEditing(_ obj: Notification) {
// Get the data when the user stopped to write
}
override func controlTextDidChange(_ obj: Notification) {
// Get the data every time the user writes a character
}

Related

NSTableView not appearing at all

I've just started working on my first project for macOS and am having trouble setting up a NSTableView. When I run it the window will appear but there is nothing in it. I've made sure all the objects have the correct class in the identity inspector and can't seem to find what I'm doing wrong.
The goal of the app is to make a notes app. I want a tableView which displays the titles of all the notes in the database, in a single column, so when you click on the cell the note will then be displayed in the rest of the window.
Here's the code:
import Foundation
import AppKit
import SQLite
class NoteCloudVC: NSViewController {
// Declare an array of Note objects for populating the table view
var notesArray: [Note] = []
// IBOutlets
#IBOutlet weak var tableView: NSTableView!
// ViewDidLoad
override func viewDidLoad() {
super.viewDidLoad()
// set the tableViews delegate and dataSource to self
tableView.delegate = self
tableView.dataSource = self
//Establsih R/W connection to the db
do {
let path = NSSearchPathForDirectoriesInDomains(
.applicationSupportDirectory, .userDomainMask, true
).first! + "/" + Bundle.main.bundleIdentifier!
// create parent directory iff it doesn’t exist
try FileManager.default.createDirectory(
atPath: path,
withIntermediateDirectories: true,
attributes: nil
)
let db = try Connection("\(path)/db.sqlite3")
//Define the Notes Table and its Columns
let notes = Table("Notes")
let id = Expression<Int64>("ID")
let title = Expression<String>("Title")
let body = Expression<String>("Body")
/*
Query the data from NotesAppDB.sqlite3 into an array of Note objs
Then use that array to populate the NSTableView
*/
for note in try db.prepare(notes) {
let noteToAdd = Note(Int(note[id]), note[title], note[body])
notesArray.append(noteToAdd)
}
} catch {
print(error)
}
}
// viewWillAppear
override func viewWillAppear() {
super.viewWillAppear()
tableView.reloadData()
}
}
// NSTableViewDataSource Extension of the NoteCloudVC
extension NoteCloudVC: NSTableViewDataSource {
// Number of rows ~ returns notesArray.count
func numberOfRows(in tableView: NSTableView) -> Int {
return notesArray.count
}
}
// NSTableViewDelegate extension of the NoteCloudVC
extension NoteCloudVC: NSTableViewDelegate {
// Configures each cell to display the title of its corresponding note
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
//configure the cell
if tableColumn?.identifier == NSUserInterfaceItemIdentifier(rawValue: "NotesColumn") {
let cellIdentifier = NSUserInterfaceItemIdentifier(rawValue: "NotesCell")
guard let noteCell = tableView.makeView(withIdentifier: cellIdentifier, owner: self) as? NotesCell else { return nil }
let note = notesArray[row]
noteCell.noteTitle.stringValue = note.title
return noteCell
}
return nil
}
}
// NotesCell class
class NotesCell: NSTableCellView {
// IBOutlet for the title
#IBOutlet weak var noteTitle: NSTextField!
}
I'm pretty familiar with UIKit so I thought the learning curve of AppKit would be a little better than SwiftUI, so if anyone could provide some guidance about where I've gone wrong that would be very much appreciated. Also if it will be a better use of my time to turn towards SwiftUI please lmk.
Here's the values while debugging:
It's reading the values from the table correctly, so I've at least I know the problem lies somewhere in the tableView functions.
The most confusing part is the fact that the header doesn't even show up. This is all I see when I run it:
Here are some images of my storyboard as well:
This is for an assignment for my software modeling and design class where my professor literally doesn't teach anything. So I'm very thankful for everyone who helps with this issue because y'all are basically my "professors" for this class. When I move the tableView to the center of the view controller in the story board I can see a little dash for the far right edge of the column but that's it, and I can't progress any further without this tableView because the whole app is dependant upon it.
So, it turns out that the code itself wasn't actually the problem. I had always used basic swift files when writing stuff for iOS so it never occured to me that I'd need to import Cocoa to use AppKit but that's where the problem lied all along. Using this code inside the auto-generated ViewController class that had Cocoa imported did the trick. Also I got rid of the extensions and just did all the Delegate/ DataSource func's inside the viewController class.

Usability of a button inside a UICollectionViewCell?

I have a ProductVC.swift (ProductViewController) file and a ProductCell.swift. The ProductVC contains a UICollectinView and ProductCell is a specific UICollectionViewCell.
ProductCell.xib looks like this:
ProductVC contains an array with all the cell data (products) and populates the cells.
My goal: The user should have the possibility to like an product. He can do it by clicking the like button on the top right corner of every cell. Every cell shows a specific product which is specified by a productID.
My Problem: The like button action (IBAction func) is in the ProductCell. ProductCell doesn´t have the cell data. Cell data is stored in ProductVC in an array. So I don´t know how catch the product(productID) the user wants to like.
My Tries: With the code below I can get the indexPath of the cell where the user clicked the like button. But I can´t use this indexPath to get the product data because the data is stored in ProductVC. I could also store the data in ProductCell but it is not a clean way. Is it possible mb to give this indexPath to the ProductVC?
extension UICollectionView {
func indexPathForView(_ view: UIView) -> IndexPath? {
let center = view.center
let viewCenter = self.convert(center, from: view.superview)
let indexPath = self.indexPathForItem(at: viewCenter)
return indexPath
}
}
let superview = self.superview as! UICollectionView
if let indexPath = superview.indexPathForView(button) {
print(indexPath) // indexPath of the cell where the button was pressed
}
SOLVED Solution is a callback closure:
//UICollectionViewCell
var saveProductLike: ((_ index: Int) -> Void)?
#IBAction func likedButtonClicked(_ sender: UIButton) {
print("Liked button clicked!")
let productArrayIndex = calculateProductArrayIndex(for: sender)
saveProductLike?(productArrayIndex!)
}
//UIViewController
cell.saveProductLike = { (index) -> Void in
print(index)
}
There are several approaches to solve this but I'll talk about the most common one which is using delegation.
protocol ProductCellDelegate: AnyObject {
func productCellDidPressLikeButton(_ cell: ProductCell)
}
in ProductCell define a property weak var delegate: ProductCellDelegate? and in the target action of the like button inform your delegate
#objc private likeButtonPressed(_ sender: Any) {
delegate?.productCellDidPressLikeButton(self)
}
In your view controller you could conform to the protocol and implement it like this:
func productCellDidPressLikeButton(_ cell: ProductCell) {
guard let ip = collectionView.indexPath(for: cell) else { return }
// process event, get product via index...
}
Then you need to set the view controller to be the delegate in collectionView(_:willDisplay:forItemAt:) or
collectionView(_:cellForItemAt:): cell.delegate = self.

Userdefaults Boolean for button

I am fairly new to Swift programming. Using Userdefaults I was trying to customize user behaviour. Below image is of my initial controller. I require to save userdefaults so that App remembers the user selection of button, (i.e. A or B). Can you assist to provide me a function that I use in viewDidLoad and it remembers the button selection and segues to its respective ViewController.
My code to perfrom segue if Button A or B is selected is
let parent = self.storyboard?.instantiateViewController(withIdentifier: "DashboardVC") as! DashboardVC
self.navigationController?.pushViewController(parent!, animated: true)
Yet it doesnt segue. It keeps loading my initial viewcontroller.
do like
set the tag for each button and create the common method for handle the function , for e.g
#IBAction func handle_Action(_ sender: UIButton) {
defaultName.set(sender.tag, forKey: "yourKeyName")
}
and in your class
class ViewController: UIViewController {
let defaultName = UserDefaults.standard
// finally access the integer in your Viewload
override func viewDidLoad() {
super.viewDidLoad()
let getVal = defaultName.integer(forKey: "yourKeyName") as Int
if getVal == 1{ //called by A
}else if getVal == 2{
//called by B
}else{ // not interactwithButton action }
}

textDidChange points to the wrong NSCollectionViewItem

I am trying to build an NSCollectionView filled with multiple editable TextViews. (OS X app in Swift.) My subclass of NSCollectionViewItem is called NoteViewItem. I am trying to have the program detect when one of the TextView has changed. I tried using both controlTextDidChange and textDidChange in the NoteViewItem's delegate with test print statement to see which would work. ControlTextDidChange did nothing; textDidChange recognized a change happened, so I went with that.
The problem is that textDidChange appears to point to a different NoteViewItem than the one that was shown on screen in the first place. It wasn't able to recognize the variable (called theNote) set in the original NoteViewItem; when I ask NoteViewItem to print String(self), I get two different results, one while setting the initial text and one in textDidChange. I'm wondering if I've set up my delegates and outlets wrongly. Any thoughts on why my references are off here?
Here's my code for NoteViewItem:
import Cocoa
class NoteViewItem: NSCollectionViewItem, NSTextViewDelegate
{
// MARK: Variables
#IBOutlet weak var theLabel: NSTextField!
#IBOutlet var theTextView: NSTextView!
var theNote: Note?
{
didSet
{
// Pre: The NoteViewItem's theNote property is set.
// Post: This observer has set the content of the *item's text view*, and label if it has one.
guard viewLoaded else { return }
if let theNote = theNote
{
// textField?.stringValue = theNote.noteText
theLabel.stringValue = theNote.filename
theTextView.string = theNote.noteText
theTextView.display()
print("theTextView.string set to "+theTextView.string!+" in NoteViewItem "+String(self))
}
else
{
theLabel.stringValue = "Empty note?"
}
}
}
// MARK: Functions
override func viewDidLoad() {
super.viewDidLoad()
// Do view setup here.
// Hopefully this will set the note's background to white.
view.wantsLayer = true
view.layer?.backgroundColor = NSColor.whiteColor().CGColor
}
// MARK: - NSTextViewDelegate
/*
override func controlTextDidChange(notification: NSNotification)
{
print("Control text changed.")
}
*/
func textDidChange(notification: NSNotification)
{
if let noteyMcNoteface = theNote
{
print("On edit, we have a note: "+String(noteyMcNoteface))
}
else
{
print("On edit, we have no note. I am NoteViewItem "+String(self))
}
}
}
I figured it out. My delegate, in the TextView, was connected to the wrong object in the Interface Builder for NoteViewItem.xib. I had connected it to the object labelled Note View Item, under objects in the outline. It should have been connected to File's Owner instead, since File's Owner stands for the NoteViewItem.swift class associated with the xib.
You'd think that if you want to connect the delegate to the NoteViewItem class and there is exactly one Note View Item listed in the outline, then that Note View Item is the thing you want to connect it to. Nope, you connect it to something entirely different that isn't called the Note View Item but is the Note View Item. I'm glad Interface Builder makes things so simple.

NSComboBox getGet value on change

I am new to OS X app development. I manage to built the NSComboBox (Selectable, not editable), I can get it indexOfSelectedItem on action button click, working fine.
How to detect the the value on change? When user change their selection, what kind of function I shall use to detect the new selected index?
I tried to use the NSNotification but it didn't pass the new change value, always is the default value when load. It is because I place the postNotificationName in wrong place or there are other method should use to get the value on change?
I tried searching the net, video, tutorial but mostly written for Objective-C. I can't find any answer for this in SWIFT.
import Cocoa
class NewProjectSetup: NSViewController {
let comboxRouterValue: [String] = ["No","Yes"]
#IBOutlet weak var projNewRouter: NSComboBox!
#IBAction func btnAddNewProject(sender: AnyObject) {
let comBoxID = projNewRouter.indexOfSelectedItem
print(“Combo Box ID is: \(comBoxID)”)
}
#IBAction func btnCancel(sender: AnyObject) {
self.dismissViewController(self)
}
override func viewDidLoad() {
super.viewDidLoad()
addComboxValue(comboxRouterValue,myObj:projNewRouter)
self.projNewRouter.selectItemAtIndex(0)
let notificationCenter = NSNotificationCenter.defaultCenter()
notificationCenter.addObserver(
self,
selector: “testNotication:”,
name:"NotificationIdentifier",
object: nil)
NSNotificationCenter.defaultCenter().postNotificationName("NotificationIdentifier", object: projNewRouter.indexOfSelectedItem)
}
func testNotication(notification: NSNotification){
print("Found Combo ID \(notification.object)")
}
func addComboxValue(myVal:[String],myObj:AnyObject){
let myValno: Int = myVal.count
for var i = 0; i < myValno; ++i{
myObj.addItemWithObjectValue(myVal[i])
}
}
}
You need to define a delegate for the combobox that implements the NSComboBoxDelegate protocol, and then use the comboBoxSelectionDidChange(_:) method.
The easiest method is for your NewProjectSetup class to implement the delegate, as in:
class NewProjectSetup: NSViewController, NSComboBoxDelegate { ... etc
Then in viewDidLoad, also include:
self.projNewRouter.delegate = self
// self (ie. NewProjectSetup) implements NSComboBoxDelegate
And then you can pick up the change in:
func comboBoxSelectionDidChange(notification: NSNotification) {
print("Woohoo, it changed")
}