NSTableView.setNeedsDisplay() not redrawing on attached Formatter changes only - swift

i am using a view based NSTableView with a column that shows dates, and the table cell views use a shared DateFormatter.
let view: NSTableCellView? = tableView.makeView(withIdentifier: column.identifier, owner: self) as! NSTableCellView?
let entry = (logController.arrangedObjects as! [LogEntry])[row]
switch column.identifier {
case columnDateKey:
view?.textField?.formatter = sharedDateFormatter
view?.textField?.objectValue = entry.date
The application has a user preference to choose the date format and previously the code
tableView.setNeedsDisplay(tableView.rect(ofColumn: tableView.column(withIdentifier: columnDateKey)))
would refresh the column with the new date format.
With macOS Mojave this does not happen. Investigation shows that although the drawRect: is called for the underlying TableView there are no calls made to tableView(:viewFor:row:) to obtain the new values for table cell views. Calling tableView.reloadData(forRowIndexes:columnIndexes:) does result in calls to tableView(:viewFor:row:) but the display does not refresh (although it does for tableView.reloadData()).
Any external cause to redraw e.g. selecting a row correctly updates that area alone. The other thing I've seen is that with a long table slowly scrolling up will eventually result in the new format appearing although existing cells do not change when scrolled back to until scrolled a long way past before returning. This would seem to infer that there are cached views that are not considered to have changed when only the configuration of the attached formatter changes (although are when the value of the contents changes)
This behaviour changed with the introduction of Mojave and I am finding it difficult to believe that no-one else has reported it and so am beginning to question my original code. Am I missing something?
The following test code demonstrates the problem, the "View requested" message is not printed for variants of setNeedsDisplay and display is only redrawn for reloadData()
styleButton is tick box to toggle number format and refreshButton is action button to request a redraw
Setting the value to a random value will result in expected redraw behaviour
import Cocoa
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
#IBOutlet weak var window: NSWindow!
#IBOutlet weak var table: NSTableView!
#IBOutlet weak var styleButton: NSButton!
#IBOutlet weak var refreshButton: NSButton!
#IBOutlet weak var testView: NSView!
let numberFormatter = NumberFormatter()
func applicationWillFinishLaunching(_ notification: Notification) {
numberFormatter.numberStyle = symbolButton.state == NSControl.StateValue.on ? NumberFormatter.Style.decimal : NumberFormatter.Style.none
}
#IBAction func refresh(sender: Any?) {
numberFormatter.numberStyle = styleButton.state == NSControl.StateValue.on ? NumberFormatter.Style.decimal : NumberFormatter.Style.none
table.setNeedsDisplay(table.rect(ofColumn: 0))
// table.needsDisplay = true
// table.reloadData(forRowIndexes: IndexSet(integersIn: 0..<table.numberOfRows), columnIndexes:[0])
// table.reloadData()
}
}
extension AppDelegate: NSTableViewDataSource {
func numberOfRows(in tableView: NSTableView) -> Int {
if tableView == table {
return 40
}
return 0
}
}
extension AppDelegate: NSTableViewDelegate {
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
print("View requested")
guard tableColumn != nil else {
return nil
}
let column = tableColumn!
if tableView == table {
let view: NSTableCellView? = tableView.makeView(withIdentifier: column.identifier, owner: self) as! NSTableCellView?
view?.textField?.formatter = numberFormatter
view?.textField?.objectValue = 123.456
return view
}
return nil
}
}

Incorrectly relying on view.setNeedsDisplay to automatically update subviews. This is not the case (although had appeared to work that way, previously) - refer comment from Willeke above

Related

NSTableView leaves "ghost cell trail" after calling reloadData()

Update: I think there's a relationship between this window's level and this problem: This never happened when this window.level was set to .normal, but when it was set to background wallpaper level.
I met a bizarre phenomenon in NSTableView behavior when I tried calling reloadData() on it. After calling reloadData(), my NSTableView correctly displayed new table cells according to the data source, but what was weird is old cells weren't completely "removed" but they kind of left their "ghost trail" or "mark" in the table view.
See the image below:
I have exhausted all methods I could think of, including prepareForReuse, I even went to the extreme thing of removing all cells from superview (NSTableView) before reloading data, and set cell.identifier = "" to force table view to create a brand new cell every reload, but that didn't work either.
My code is very simple as below
Controller code:
// TodoListTableViewController.swift
eventStore.fetchReminders(matching: predicateIncompleteReminders) { reminders in
self.reminders = reminders
DispatchQueue.main.async { [self] in
todoListTableView.reloadData()
}
}
// still in TodoListTableViewController.swift
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
let reminder = reminders[row]
let itemCell = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: CellViewIdentifierSet.TodoItemCellView.rawValue), owner: self) as! TodoItemCellView
itemCell.todoItemTextField.stringValue = reminder.title
itemCell.todoColorTextField.textColor = reminder.calendar.color
if let date = reminder.dueDateComponents?.date {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "HH:mm"
itemCell.todoStartTimeTextField.stringValue = dateFormatter.string(from: date)
} else {
itemCell.todoStartTimeTextField.stringValue = ""
}
return itemCell
}
TodoItemCellView code:
import Cocoa
class TodoItemCellView: NSTableCellView {
#IBOutlet var todoColorTextField: NSTextField!
#IBOutlet var todoItemTextField: NSTextField!
#IBOutlet var todoStartTimeTextField: NSTextField!
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
// Drawing code here.
todoItemTextField.maximumNumberOfLines = 2
}
override func prepareForReuse() {
super.prepareForReuse()
todoItemTextField.stringValue = ""
todoStartTimeTextField.stringValue = ""
}
}
I'm pretty confident that logically nothing can be wrong.
I also double checked that only one table view in my storyboard and no double stacked views.
I'm running the app on my MacBook Pro (mid 2015).
Edit: I did a view hierarchy debugging session. The view hierarchy was like this:
However, what my app displayed looked like this (notice the blurred trail)
Is it safe to assume that my MacBook Pro 2015 has a poor-quality display that can't properly refresh the content? And won't this behavior show up on other people's computers?

How do I use UIStepper to change a property in a Realm Model Object?

I currently have a ViewController with a TableView inside it called SelectedListItemsViewController. This ViewController's TableView is populated by a custom cell class called SelectedListItemsTableViewCell.
I have an array of Realm Model Objects called selectedListItems, each of which has several properties. The SelectedListItemsTableViewCell populates the TableView with the listItem property of that indexPath.row's object, and each row has a UIStepper with a UILabel next to it that (as of now) shows UIStepper.value for each row. Ideally, the label will reflect the listItemWeight property of each row, and change it when incrementing or decrementing that row.
This is my custom cell:
class SelectedListItemsTableViewCell: UITableViewCell {
#IBOutlet var selectedListItemLabel: UILabel!
#IBOutlet weak var listItemWeightLabel: UILabel!
#IBOutlet weak var stepperControl: UIStepper!
#IBAction func stepperValueChanged(sender: UIStepper) {
listItemWeightLabel.text = Int(sender.value).description
}
}
And in my ViewController's cellForRowAtIndexPath, I've configured the cell like so:
// Configure the cell...
cell.selectedListItemLabel.text = selectedListItems[indexPath.row].listItem
cell.listItemWeightLabel.text = "\(selectedListItems[indexPath.row].listItemWeight)"
Which perfectly loads the listItem property, and the listItemWeight property shows up correctly, but as soon as I increment or decrement on the UIStepper it gets messed up.
How do I properly link my UILabel and UIStepper to the [indexPath.row].listItemWeight?
In the same method that gets called when your stepper updates, update your listItem. However, since this item is stored in your Realm database, you will have to get an instance of your Realm database and write the change to the database.
You can do this by having your TableViewCell hold on to an instance of the listItem. Your new TableViewCell class will look something like this:
class SelectedListItemsTableViewCell: UITableViewCell {
#IBOutlet var selectedListItemLabel: UILabel!
#IBOutlet weak var listItemWeightLabel: UILabel!
#IBOutlet weak var stepperControl: UIStepper!
var listItem: Item?
#IBAction func stepperValueChanged(sender: UIStepper) {
listItemWeightLabel.text = Int(sender.value).description
if let listItem = listItem {
let realm = try! Realm
try! realm.write {
listItem.listItemWeight = Int(sender.value) ?? 0
}
}
}
}
The above answer was helpful in leading me to the actual solution of my issue, but if anyone in the future is curious - I ended up using a closure.
In my custom cell class, I did
#IBAction func stepperValueChanged(sender: UIStepper) {
selectedListItemLabel.text = Int(sender.value).description
tapped?(self)
}
And in my view controller, I did
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! SelectedListItemsTableViewCell
cell.stepperControl.value = selectedListItems[indexPath.row].listItemWeight
// Configure the cell...
cell.tapped = { (selectedCell) -> Void in
selectedListItems[indexPath.row].listItemWeight = cell.stepperControl.value
}
Which allowed me to access each cell's UIStepper in the view controller file.
It helped to read flashadvanced's Option 2 answer in this thread.

Using the contentView property on a custom tableView cell (being passed as a header) how to prevent it from nullifying the custom attributes?

For example here is my custom cell:
protocol SectionHeaderTableViewCellDelegate {
func didSelectUserHeaderTableViewCell(sectionHeader: SectionHeaderTableViewCell, selected: Bool, type: Type)
}
class SectionHeaderTableViewCell: UITableViewCell {
#IBOutlet weak var labelContainerView: LabelContainerView!
#IBOutlet weak var sectionTitleLabel: UILabel!
#IBOutlet weak var plusButton: UIButton!
var type: Type?
var delegate: SectionHeaderTableViewCellDelegate?
var dog: Dog?
let sections = [Type.Meals, Type.Exercise, Type.Health, Type.Training, Type.Misc]
}
extension SectionHeaderTableViewCell {
#IBAction func plusButtonPressed(sender: AnyObject) {
if let type = type {
delegate?.didSelectUserHeaderTableViewCell(self, selected: plusButton.selected, type: type )
}
}
In my controller if I add a return of header.contenView I get the desired results of the header staying in place but unfortunately it nullifies the button included in the custom header preventing it from being called. Otherwise if I simply just return header the button on the custom header cell works as expected but the header moves with the row being deleted which is obviously unsightly and not what I want.
func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
guard let header = tableView.dequeueReusableCellWithIdentifier("sectionHeader") as? SectionHeaderTableViewCell else { return UITableViewCell() }
header.delegate = self
header.updateDogWithGender(dog)
header.type = header.sections[section]
header.sectionTitleLabel.text = header.sections[section].rawValue
return header.contentView
}
moving headers
In case anyone runs into a similar situation the solution was to create a Nib file and customize it as you see fit. Create a nib file by going to File -> New File -> iOS -> User Interface -> and selecting View. Create Nib file. I added my views and buttons to get the look I wanted. customize Nib. From there I changed the custom cell class to be UITableViewHeaderFooterView instead and reconnected my outlets and actions to the new Nib file.
class SectionHeaderView: UITableViewHeaderFooterView {... previous code from above }
Back in the controller update the viewForHeaderInSection function to load a nib instead :
func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let header = NSBundle.mainBundle().loadNibNamed("SectionHeader", owner: self, options: nil).first as? SectionHeaderView
header?.delegate = self
header?.updateDogWithGender(dog)
header?.type = header?.sections[section]
header?.sectionTitleLabel.text = header?.sections[section].rawValue
return header
}
By the way we declared the property first at the end of the loadNibNamed property because it returns an array of AnyObjects and since my Nib file only contains one UIView that houses a label and a button I only needed the first and only item in the array. Thanks to my mentor James for figuring this out!

OSX view-based NSTableView font change

I have an OS X application written in Swift (thanks to Mathias and Gallagher) that uses a cell-based NSTableView. One of the requirements by the client was to be able to increase the font size of the text displayed in each text field cell. It all seemed pretty straight forward after a bit of stack overflow googling: set each NSTableView column’s dataCell font to the desired font and size; then subclass the NSTextFieldCell and override the drawInteriorWithFrame and titleRectForBounds and adjust the height to fit the rect.
However, since Apple has depreciated cell-based NSTableViews in favor of view-based I figured I should change my code to view-based.
Argh! What seemed like such a simple change has caused me two days of hair pulling grief. I can get the text font size to change just fine but the NSTextFieldCell NSRect height stays fixed. What few examples I’ve seen on the web are for iOS and don’t work for OS X.
Is there an easy way to do this?
I marked your question as favorite, tried it, failed, and slept on it for a long time. But I think I've solved it for all future Googlers.
You shouldn't have to subclass anything for this. Try this:
In Interface Builder:
Select the Table View Cell (#1 in the screenshot), open the Identity Inspector (Cmd + Opt + 3) and set its Identifier to myTableViewCell
Select the Text Cell (#2 in the screenshot), open the File Inspector (Cmd + Opt + 1) and uncheck Use Auto Layout
In your view controller:
Connect the outlets and actions and you should be fine:
class AppDelegate: NSObject, NSApplicationDelegate, NSTableViewDataSource, NSTableViewDelegate {
#IBOutlet weak var window: NSWindow!
#IBOutlet weak var tableView: NSTableView!
var names = ["Luke Skywalker", "Han Solo", "Chewbecca"]
var fontSize = NSFont.systemFontSize()
func applicationDidFinishLaunching(aNotification: NSNotification) {
self.tableView.setDataSource(self)
self.tableView.setDelegate(self)
}
func numberOfRowsInTableView(tableView: NSTableView) -> Int {
return self.names.count
}
func tableView(tableView: NSTableView, viewForTableColumn tableColumn: NSTableColumn?, row: Int) -> NSView? {
let cellView = tableView.makeViewWithIdentifier("myTableViewCell", owner: self) as! NSTableCellView
let textField = cellView.textField!
let fontDescriptor = textField.font!.fontDescriptor
textField.font = NSFont(descriptor: fontDescriptor, size: self.fontSize)
textField.stringValue = self.names[row]
textField.sizeToFit()
textField.setFrameOrigin(NSZeroPoint)
tableView.rowHeight = textField.frame.height + 2
return cellView
}
#IBAction func makeBigger(sender: AnyObject) {
self.fontSize += 1
self.tableView.reloadData()
}
#IBAction func makeSmaller(sender: AnyObject) {
self.fontSize -= 1
self.tableView.reloadData()
}
}

Custom cell: fatal error: unexpectedly found nil while unwrapping an Optional value

I have a table view with custom cell that was created as .xib . I didnt use storyboard. I have a problem that I couldnt fill my table with the data which came from webservice result. Also, I have 4 labels in the custom cell. In my custom cell class, when I try to set labels for each items, It gives me fatal error like above.
Here is my code:
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
...
func tableView(tableView: UITableView!, cellForRowAtIndexPath indexPath: NSIndexPath!) -> UITableViewCell!
{
let cell: ItemManagementTVCell = tableView?.dequeueReusableCellWithIdentifier("cell") as ItemManagementTVCell
if let ip = indexPath
{
let item: Item = self.itemList[indexPath.row] as Item
cell.setCell(item.itemName, status: item.itemStatus, duration: item.itemDuration, price: item.itemPrice)
}
return cell
}
}
And my custom cell class is here :
import UIKit
class ItemManagementTVCell: UITableViewCell {
#IBOutlet var lblItemName: UILabel!
#IBOutlet var lblItemPrice: UILabel!
#IBOutlet var lblItemDuration: UILabel!
#IBOutlet var lblItemStatus: UILabel!
override func awakeFromNib()
{
super.awakeFromNib()
}
override func setSelected(selected: Bool, animated: Bool)
{
super.setSelected(selected, animated: animated)
}
func setCell(name: String, status: Int, duration: Int, price: Int)
{
self.lblItemName.text = name
self.lblItemStatus.text = String(status)
self.lblItemDuration.text = "Duration: \(String(duration)) months"
self.lblItemPrice.text = String(price) + " $"
}
}
I am getting the error inside of "setCell" method block.
I have read a lot of questions and solutions and I tried all of them it doesnt work for me.
Thank you for your answers,
Best regards.
SOLUTION: I've solved this problem by linking the cell items to cell's own instead of linking to File's Owner. My problem has gone by doing this.
Another solution to the problem without having to link cell items to the cell owner:
let nib = UINib(nibName: "YOUR_CUSTOM_CELL_NIB_NAME", bundle: nil)
tableView.register(nib, forCellReuseIdentifier: "YOUR_CUSTOM_CELL_ID")
Your "cell" must be nil.
Using
tableView.dequeueReusableCellWithIdentifier("cell") as ItemManagementTVCell
Can return nil. You should use:
tableView.dequeueReusableCellWithIdentifier("cell" forIndexPath:indexPath) as ItemManagementTVCell
This way it guarantees cells is not nil.
EDIT: Maybe you can prevent the crash by putting if statements inside "setCell"
if var itemName = self.lblItemName {
itemName.text = name
}
Do that for every label you set inside it and check if the crash still happens. If it don't you must check why those labels are nil.