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

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?

Related

Custom UITableViewCell changes width randomly (swift)

I'm making a chess app for swift with a chat function. I upload my messages to firebase firestore and then, download them into a [Message] array and display them in a UITableView.
I've been looking around for this on stackoverflow and found that several people are having trouble with sizes of a custom UITableViewCell. I've run into the same problem, but in my case the width of the cell seems to randomly change every time I call .reloadData(). To illustrate :
The first 2 times or so when I type something in my chat it should look like this (the cells in the circle)
But as you can see after a few times this happens :
My code :
Custom cell class
class MessageCustomCell: UITableViewCell {
#IBOutlet weak var dateLabel: UILabel!
#IBOutlet weak var bgView: UIView!
#IBOutlet weak var leftImage: UIImageView!
#IBOutlet weak var messageBubble: UIView!
#IBOutlet weak var messageLabel: UILabel!
#IBOutlet weak var rightImage: UIImageView!
override func awakeFromNib() {
super.awakeFromNib()
messageLabel.numberOfLines = 0
messageBubble.layer.cornerRadius = 5
}
}
TableView delegate methods (TableView = messageView)
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return allMessages.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = messageView.dequeueReusableCell(withIdentifier: "MessageCustomCell") as! MessageCustomCell
//cell.width = tableView.frame.width
let msg = allMessages[indexPath.row]
//cell.frame.size.width = messageView.frame.width
//cell.contentView.frame.size.width = messageView.frame.width
let date = msg.time
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "d/M/yy, h:mm"
let realDate = dateFormatter.string(from: date)
print(msg.fromUserName!)
if msg.fromUserName! == currentUserName {
cell.messageBubble.backgroundColor = UIColor(named: "lightSquare")
cell.messageLabel.textColor = .black
cell.rightImage.isHidden = true
} else {
cell.messageBubble.backgroundColor = UIColor(named: "darkSquare")
cell.messageLabel.textColor = .white
cell.leftImage.isHidden = true
}
cell.dateLabel.text = realDate
cell.messageLabel.text = msg.text!
return cell
}
In my MessageCustomcell.xib file I have a few constraints :
I've put the leftImage, messageBubble (UIView), rightImage in a horizontal stackview. The images have only width and height constraints. The label in the messageBubble has constraints to its superview (top,bottom,trailing,leading)
The stackView has constraints to the bgView (UIView), also top bottom trailing and leading.
Same for the bgView to the safe area.
I've tried setting the frame for the custom cell, but that didn't seem to work, so I've commented that out.
Really hope someone can help me out. If you guys need more info then please do let me know :)
Thanks all.
EDIT :
Here are my constraints for the custom cell xib file.
Alright thanks to #SPatel, who pointed out to make 2 separate .xib files for the receiver chat bubble and the sender chat bubble. This actually worked for my situation and was the right way to go in the first place, instead of manipulating one custom cell for both sender and receiver.
I'm thinking the constraint issue had to do with me trying to hide either the right image or the left image, to change the custom cell appearance for both sides. That must have messed up the whole constraints situation for the custom cell.
Anyway, happy man. Moving on!

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

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

How to make some specific items of a NSTableView in bold?

I would like to set some items of a non-editable, View Based NSTableView in bold. The items correspond to a specific index number of the array I use to populate the TableView.
I would like to set the change before the NSTableView is displayed to the users.
I tried to handle this change in this method but I can't find a way to do it:
func tableView(_ tableView: NSTableView, objectValueFor tableColumn: NSTableColumn?, row: Int) -> Any?
If you don't want to use Cocoa Bindings
It's very similar on how you do it on iOS. Configure the cell's view in tableView(_:viewFor:row:)
class ViewController: NSViewController, NSTableViewDataSource, NSTableViewDelegate {
#IBOutlet weak var tableView: NSTableView!
var daysOfWeek = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]
var boldDays = ["Monday", "Wednesday"]
override func viewDidLoad() {
self.tableView.dataSource = self
self.tableView.delegate = self
}
func numberOfRows(in tableView: NSTableView) -> Int {
return daysOfWeek.count
}
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
// Assuming that you have set the cell view's Identifier in Interface Builder
let cell = tableView.make(withIdentifier: "myCell", owner: self) as! NSTableCellView
let day = daysOfWeek[row]
cell.textField?.stringValue = day
if boldDays.contains(day) {
let fontSize = NSFont.systemFontSize()
cell.textField?.font = NSFont.boldSystemFont(ofSize: fontSize)
// if you require more extensive styling, it may be better to use NSMutableAttributedString
}
return cell
}
}
If you want to use Cocoa Bindings
Cocoa Bindings can make this very simple, but if you set the slightest things wrong, it's pretty hard to figure out where things went south. Heed the warning from Apple:
Populating a table view using Cocoa bindings is considered an advanced topic. Although using bindings requires significantly less code—in some cases no code at all—the bindings are hard to see and debug if you aren’t familiar with the interface. It’s strongly suggested that you become comfortable with the techniques for managing table views programmatically before you decide to use Cocoa bindings.
Anyhow, here's how to do it. First, the code:
// The data model must inherit from NSObject for KVO compliance
class WeekDay : NSObject {
var name: String
var isBold: Bool
init(name: String, isBold: Bool = false) {
self.name = name
self.isBold = isBold
}
}
class ViewController: NSViewController, NSTableViewDataSource, NSTableViewDelegate {
#IBOutlet weak var tableView: NSTableView!
let daysOfWeek = [
WeekDay(name: "Sunday"),
WeekDay(name: "Monday", isBold: true),
WeekDay(name: "Tuesday"),
WeekDay(name: "Wednesday", isBold: true),
WeekDay(name: "Thursday"),
WeekDay(name: "Friday"),
WeekDay(name: "Saturday")
]
override func viewDidLoad() {
self.tableView.dataSource = self
self.tableView.delegate = self
}
}
Then the Interface Builder config:
Make sure the Table Column and Table Cell View have the same identifier. Best is to leave both of them blank for automatic
Select the Table View, bind Table Content to View Controller, Model Key Path = self.daysOfWeek
Select the Table View Cell, bind Value to Table Cell View (no kidding), Model Key Path = objectValue.name
Scroll down, bind Font Bold to Table Cell View, Model Key Path = objectValue.isBold
Either way, you should end up with something like this:
Polish as needed.

Tab Bar Item hidden behind tableview / not being shown?

I have an empty view with a tab bar pictured below, when i load a routine a table appears containing the contents, however it seems to overlay the tab bar killing off app navigation. Its not sized in the storyboard to overlay it and its constraint locked to not do so, so im unsure why this is happening, pics of the issue and VC's code below:
VC Code:
import Foundation
import UIKit
import CoreData
class RoutineController: UIViewController, UITableViewDataSource, UITableViewDelegate {
// MARK: - DECLARATIONS
#IBAction func unwindToRoutine(segue: UIStoryboardSegue) {}
#IBOutlet weak var daysRoutineTable: UITableView!
#IBOutlet weak var columnHeaderBanner: UIView!
#IBOutlet weak var todaysRoutineNavBar: UINavigationBar!
#IBOutlet weak var addTOdaysRoutineLabel: UILabel!
let date = Date()
let dateFormatter = DateFormatter()
let segueEditUserExerciseViewController = "editExerciseInRoutineSegue"
//This is the selected routine passed from the previous VC
var selectedroutine : UserRoutine?
// MARK: - VIEWDIDLOAD
override func viewDidLoad() {
super.viewDidLoad()
setupView()
daysRoutineTable.delegate = self
daysRoutineTable.dataSource = self
view.backgroundColor = (UIColor.customBackgroundGraphite())
dateFormatter.dateStyle = .short
dateFormatter.dateFormat = "dd/MM/yyyy"
let dateStr = dateFormatter.string(from: date)
todaysRoutineNavBar.topItem?.title = dateStr + " Routine"
}
// MARK: - VIEWDIDAPPEAR
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.daysRoutineTable.reloadData()
self.updateView()
}
// MARK: - TABLE UPDATE COMPONENTS
private func setupView() {
updateView()
}
// MARK: - TABLE SETUP
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if let count = self.selectedroutine?.userexercises?.count
{
print("exercises: \(count)")
return count
}
return 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as? TodaysRoutineTableViewCell else {
fatalError("Unexpected Index Path")
}
cell.backgroundColor = UIColor.customBackgroundGraphite()
cell.textLabel?.textColor = UIColor.white
configure(cell, at: indexPath)
return cell
}
// MARK: - VIEW CONTROLER ELEMENTS VISIBILITY CONTROL
fileprivate func updateView() {
var hasUserExercises = false
if let UserExercise = self.selectedroutine?.userexercises {
hasUserExercises = UserExercise.count > 0
}
addTOdaysRoutineLabel.isHidden = hasUserExercises
columnHeaderBanner.isHidden = !hasUserExercises
daysRoutineTable.isHidden = !hasUserExercises
}
// MARK: - SETTING DATA FOR A TABLE CELL
func configure(_ cell: TodaysRoutineTableViewCell, at indexPath: IndexPath) {
if let userExercise = selectedroutine?.userexercises?.allObjects[indexPath.row]
{
print("\((userExercise as! UserExercise).name)")
cell.todaysExerciseNameLabel.text = (userExercise as! UserExercise).name
cell.todaysExerciseRepsLabel.text = String((userExercise as! UserExercise).reps)
cell.todaysExerciseSetsLabel.text = String((userExercise as! UserExercise).sets)
cell.todaysExerciseWeightLabel.text = String((userExercise as! UserExercise).weight)
}
}
}
requested table constraints
Debug hierarchy
The Segue that sends the user back to the view that looses its tab bar
if segue.identifier == "addToTodaySegue" {
let indexPath = workoutTemplateTable.indexPathForSelectedRow
let selectedRow = indexPath?.row
print("selected row\(selectedRow)")
if let selectedRoutine = self.fetchedResultsController.fetchedObjects?[selectedRow!]
{
if let todaysRoutineController = segue.destination as? RoutineController {
todaysRoutineController.selectedroutine = selectedRoutine
}
}
}
I also feel perhaps the viewDidAppear code may cause the issue, perhaps the super class?
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.daysRoutineTable.reloadData()
self.updateView()
Updated storyboard image
I suspect you need to embed your viewController in a UINavigationController.
Consider the following setup:
I suspect your setup is like the upper one:
TapBar -> ViewController -show segue-> ViewController
Which results in a hidden tapbar, like in your description:
While the bottom setup:
TapBar -> NavigationCntroller -rootView-> ViewController -show segue-> ViewController
results in:
which is what you want, how I understood.
Update
It's hard to see. The screenshot of your Storyboard is in pretty low resulution, but the segues look wrong. Double check them. A Segue of type show (e.g push) looks like this:
Also clear project and derived data. Segue type changes sometime are ignored until doing so.
Try calling this self.view.bringSubviewToFront(YourTabControl).
The previous suggestion should work. But the content at the bottom part of tableview will not be visible as the tabbar comes over it. So set the bottom constraint of tableview as the height of tabbar.

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()
}
}