NSTableView Custom Row View Caching - swift

I've been customising my NSTableRowView using rowViewForRow, but this has led to a serious performance issue when scrolling as the rows are constantly redrawn.
I tried updating my code to only redraw if the row doesn't exist which gives much faster/smoother scrolling, but sometimes some views now display the wrong content.
func tableView(_ tableView: NSTableView, rowViewForRow row: Int) -> NSTableRowView? {
let identifier = NSUserInterfaceItemIdentifier("MyTableRowView")
var rowView = tableView.makeView(withIdentifier: identifier, owner: self) as? MyTableRowView
// does the row exist?
if rowView == nil {
// create the row view
rowView = MyTableRowView(frame: NSRect.zero)
// set the identifier
rowView?.identifier = identifier
}
return rowView
}
Any suggestions much appreciated!

Figured this out. The function was working fine, but one of my custom button views required setNeedsDisplay on isChecked to update the display. This has previously always worked because the row had been redrawn all the time.

Related

Protocol Doesn't Send Value to Other VC

That is my footerView called FooterTableViewCell. I have this protocol called SurveyAnswerTableViewCellDelegate. It's parent is AddQuestionViewController.
When I tap on the footerView I trigger #IBActtion.
#objc protocol SurveyAnswerTableViewCellDelegate: AnyObject {
func textSaved(_ text: String)
}
class FooterTableViewCell: UITableViewHeaderFooterView {
var parentVC: AddQuestionViewController!
#IBAction func addNewTapped(_ sender: Any) {
print("tapped")
let newTag = model.tag + 1
parentVC.addNewAnswer()
}
This button action triggers AddQuestionViewController
class AddQuestionViewController: SurveyAnswerViewDelegate, UITextFieldDelegate, UITableViewDelegate, SurveyAnswerTableViewCellDelegate {
var answers: [SurveyAnswerModel] = []
var savedText : String = ""
static var delegate: SurveyAnswerTableViewCellDelegate?
I try creating an empty string and append a new answer to my array. But this text here is always "".
func addNewAnswer() {
let newAnswer = SurveyAnswerModel(answer: savedText, tag: 0)
self.answers.append(newAnswer)
self.tableView.reloadData()
}
func textSaved(_ text: String) {
savedText = text
}
The textfield I try to read is inside SurveyAnswerTableViewCell while setting up the cell inside the tableview I call setup function.
class SurveyAnswerTableViewCell: UITableViewCell {
#IBOutlet weak var textField: UITextField!
weak var delegate: SurveyAnswerTableViewCellDelegate?
var parentVC: AddQuestionViewController!
func setup() {
if let text = self.textField.text {
self.delegate?.textSaved(textField.text!)
}
}
extension AddQuestionViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(forIndexPath: indexPath) as SurveyAnswerTableViewCell
cell.parentVC = self
cell.setup()
return cell
}
How can I successfully send that text to AddQuestionViewController so it appends a new answer with correct string
There are a few things keeping this from working.
You are calling SurveyAnswerTableViewCell's setup() function directly after dequeuing the cell for reuse. It has not yet (re)appeared on the screen at that point, so the user has not had a chance to enter anything into the text field.
You don't currently set the delegate property of SurveyAnswerTableViewCell to anything, so even if the textfield had valid input, the delegate would be nil and delegate?.textSaved(textField.text!) wouldn't do anything.
Both of the previous points mean that the value of AddQuestionViewController .savedText never gets updated from the empty string. So when addNewAnswer() tries to read it, it will always see that empty string.
Rather than reading the text field when the cell is dequeued, it would make more sense to save the text field value when the user is done typing.
To do that, conform the cell to UITextFieldDelegate and implement the textFieldDidEndEditing(_:) method. From within that method you can then call the delegate method you already have to save the text. Make sure the delegate property on the cell has been set by the VC, or else this won't do anything!
The VC itself should not have a delegate property of type SurveyAnswerTableViewCellDelegate. It serves as the delegate, rather than having one. If this doesn't quite make sense, I would recommend reviewing some online resources on the delegate pattern.
So make sure the ViewController conforms to SurveyAnswerTableViewCellDelegate and then set the cell's delegate value to the VC. The cellForRowAt function should then look something like this:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(forIndexPath: indexPath) as SurveyAnswerTableViewCell
cell.delegate = self
return cell
}
As a side note, neither the footer nor the cell should have a reference to the parent view controller. as a general rule it is good to avoid subviews being aware of their parent views. Things get unnecessarily complicated when there is two-way knowledge sharing between components, and it makes the subview much less reusable. I would recommend making a delegate for the footer as well, and removing the parentVC property from both the footer and the cell.
Here's what it looks like is happening:
Button tapped
addNewTapped(_:) invoked
addNewAnswer() invoked
newAnswer is appended to answers
tableView.reloadData() invoked
Cells are regenerated with new/empty textfields (so delegate.textSaved is never invoked)
so I'm not sure what you're trying to do, but here's what I figure are a couple possible routes:
store UITextFields separately and add them into table cells so they're not removed by a table reload
conform AddQuestionViewController to UITextFieldDelegate and set it as the textfields' delegate to observe textfield texts changing (and if you're only using 1 textfield, you could set savedText there)

View-based NSTableView not applying partial reloads with DifferenceKit

I'm using an NSTableView and DifferenceKit. This is all programmatic, no Interface Builder at all.
I'd previously implemented only tableView(_:objectValueFor:row) in order to get values into my table. At that point I could apply full and partial reloads and everything worked fine.
Now I've added an implementation of tableView(_:viewFor:row:) in order to format some columns differently, and it's affected reloading. A full reloadData() still works, but a call to reloadData(forRowIndexes:columnIndexes) doesn't call either my datasource or delegate methods; the reload seems to simply disappear.
I also tried removing the datasource method and running only with tableView(_:viewFor:row:) but no dice. A partial reload still doesn't call the delegate method.
Has anyone come across this? Is there a nuance of NSTableView I'm missing?
My code (truncated):
init() {
...
tableView.dataSource = self
tableView.delegate = self
columns.forEach {
tableView.addTableColumn($0)
}
}
func tableView(
_ tableView: NSTableView,
objectValueFor tableColumn: NSTableColumn?,
row: Int
) -> Any? {
...
return someStringProvider(column, row)
}
func tableView(
_ tableView: NSTableView,
viewFor tableColumn: NSTableColumn?,
row: Int
) -> NSView? {
...
if let existingView = tableView.makeView(withIdentifier: identifier, owner: self) as? NSTableCellView {
existingView.textField?.stringValue = someStringProvider(column, row)
return existingView
}
let textField = NSTextField()
...
textField.stringValue = someStringProvider(column, row)
let view = NSTableCellView()
view.identifier = identifier
view.addSubview(textField)
view.textField = textField
view.addConstraints([
... (pin textField to view)
])
textField.bind(
.value,
to: view,
withKeyPath: "objectValue",
options: nil
)
return view
}
This turns out to be either a misunderstanding of, or possibly a bug in, the way DifferenceKit interacts with a view-based, rather than cell-based, NSTableView.
I was crucially not checking the values with which reloadData(forRowIndexes:columnIndexes) was called, and the columnIndexes passed by DifferenceKit's algorithm were always [0], which was not valid for any visible cells as my first column is hidden, so the methods weren't called.
The useful lesson here is that when a tableview's delegate lacks tableView(_:viewFor:row:), a reload to any column reloads the entire row, but once views are specified, the column values become relevant.

How do I access an ImageView within an NSTableCellView in Swift?

I'm trying to programmatically populate the cells of a column with images already defined in an array (flags).
In IB I have an Image View directly within a Table Cell View. In the ViewController this is what I'm doing:
public func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
guard let cellView = tableView.makeView(withIdentifier: tableColumn!.identifier, owner: self) as? NSTableCellView else { return nil }
if tableColumn?.title == "flag" {
cellView.imageView?.image = NSImage(named: flags[row]) // NOT WORKING
} else if tableColumn?.title == "country" {
cellView.textField?.stringValue = flags[row].uppercased()
}
return cellView
}
This results in the text field cells in the "country" column getting set fine, but each of the cells in the "flag" column all have the default image I set in IB--or nothing if I take that out.
According to the tutorials and StackOverflow posts I've looked through, it seems that I'm doing everything right--but obviously I've messed up something.
(In response to the screen cast shared in the comments)
Notice how that Image View (in the cell) is not referenced by anything (its "Referencing Outlets" section on the right is empty).
Your image view is a subview of its cell (as indicated by its position in the view heiarchy on the left side bar), so you could presumably access it using something like:
let imageView = cell.subViews.first(where: { $0 is NSImageView }) as! NSImageView
imageView.image = NSImage(named: flags[row])
...but that's obviously clunky.
Instead, you should select your cell, and drag from its imageView outlet, to the Image View. This will make a new reference, which will make cell.imageView non-nil.
Alternatively, I would suggest you just delete your cell, and create a new one from the "Image & Test Table Cell View" from the "Object Library". It'll have all the outlets hooked up already.

Text is jumbled in custom cell

I'm trying to read data from CoreData into a custom cell. This is just a test app before I try moving to the real app that I've been working on. The data is there - I can print it to the console and see it. For some reason, even with constraints, all of the data is laid on top of each other in the cell. Can anyone see what's going on with this?
I've created constraints to keep the cells where they should be, but when the data is loaded from my 'show data' button, the data is laid on top of each other.
Here is my custom cell class:
import UIKit
class CustomCellClass: UITableViewCell {
#IBOutlet weak var txtNameLabel: UILabel!
#IBOutlet weak var txtAgeLabel: UILabel!
}
Here is the ShowData class: (partial)
class ShowData: UIViewController, NSFetchedResultsControllerDelegate {
#IBOutlet weak var personTableView: UITableView!
let appDelegate = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
var personData = [Person]()
// Read the data
override func viewDidLoad() {
super.viewDidLoad()
personTableView.delegate = self
personTableView.dataSource = self
loadItems()
}
func loadItems() {
let request : NSFetchRequest<Person> = Person.fetchRequest()
do {
personData = try appDelegate.fetch(request)
} catch {
print("couldn't load")
}
}
}
extension ShowData : UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return personData.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let person = personData[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: "personNameAge", for: indexPath) as! CustomCellClass
cell.txtNameLabel?.text = person.name
cell.txtAgeLabel?.text = String(person.age)
return cell
}
Here is a screenshot of the tableview while running:
Edit:
I just deleted the app from the simulator and tried to rerun - now there isn't any data in the cells.
Just to clarify for other readers, as can be seen in your screenshot, the rows in your table are separated as expected but the different fields in each cell, what one might call the columns, are on top of one another.
You say that you have created constraints to keep the cells where they should be, I'm not sure what you mean by that. What you need is constraints for the fields within each cell – what I call intra-cell constraints. Either you have not added these constraints, or there is a mistake in them which causes all fields to be drawn at the left.
To show you what I mean, let's use the example of a little workout app of mine which has, in each table cell, from left to right, a Perform button, an Edit button, a Name field and a Duration field. The screenshot below shows, in the big yellow box, the intra-cell constraints. If you are using a storyboard, the problem with your app must be in that area. If you are not using a storyboard, the problem must be in the equivalent code (or lack of it).
Just to let everyone know. The issue is resolved. I removed the table view cell from the project, readded, and readded the constraints. Everything is working now. I'm not sure where the problem was, but I noticed I had weird wrapping happening. I moved one of the labels to the other side of the cell and constrained it to the right side, and the other to the left side. When I ran the app, the text appeared to word wrap. I decided to delete the cell and readd and relink my outlets. It worked the first time...

Why is NSTableView not reloading?

I'm still very new to programming in Swift, (and never with Objective C). What I'm trying to do is add to the NSTableView when I've clicked on an item in the current tableview. The items seem to be adding when clicked, but the table does not seem to be refreshing with the new things in the array.
I've tried various things over the last few days, getting it to run reloadData on main thread and UI thread, etc but I'm feel like I'm just hitting the wall (it surely can't be this hard to do something so simple like I can in a couple minutes in Java)....
Have I missed something very obvious? (Code below)
class TableViewController: NSTableView, NSTableViewDataSource, NSTableViewDelegate {
var items: [String] = ["Eggs", "Milk"]
func tableView(tableView: NSTableView, viewForTableColumn tableColumn: NSTableColumn?, row: Int) -> NSView? {
let result : TableCell = tableView.makeViewWithIdentifier(tableColumn!.identifier, owner: self) as! TableCell
result.itemField.stringValue = items[row]
return result
}
func numberOfRowsInTableView(tableView: NSTableView) -> Int {
return items.count
}
func tableViewSelectionDidChange(notification: NSNotification) {
let index = notification.object!.selectedRow
if (index != -1) {
NSLog("#%d", index)
items.append("Blah")
NSLog("#%d", items.count)
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.reloadData()
})
}
}
func relloadData() { //relload is not a typo
self.reloadData()
}
}
If you're binding your user interface to the items property of your view controller subclass, you need to mark it as it a Dynamic Variable:
dynamic var items: [String] = ["Eggs", "Milk"]
Place the dynamic keyword before the property declaration and I think it will solve your problem.
Note that Ken's comment also makes a good point in that this code probably should be written as an NSViewController subclass, instead of a subclass of NSTableView.
If you're using Storyboards, the custom view controller subclass would be a View Controller object for the view containing your table view. If you're using a xib file for the view, the view controller subclass would be the File's Owner. In either case, you would connect the table view's delegate and data source outlets to that object.