OSX view-based NSTableView font change - swift

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

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?

Unable to populate xib-created swift tableView using macOS

I am unable to populate a swift cell-based tableview in macOS 10.14.6 using an Xcode 11.2 xib. The app is Document based and the tableView is created with a separate WindowController xib. A similar project created programmatically in Xcode works ok, including drag and drop; I am relatively new to using xibs and likely have not set things correctly. A column identifier has been set in the xib and NSTableViewDataSource and NSTableViewDelegate have been added to the Window Controller. Pertinent source code follows and the complete Xcode project may be downloaded here: https://www.dropbox.com/s/6tsb98b7iihhfxl/tableView.zip?dl=0
Any help in getting the tableView populated with a String array would be appreciated. I would also like to get drag and drop working but can get by for now just getting the array items to show up in the table view. It correctly creates four rows, corresponding to the number of elements in the array, but there is no visible text. The tableView is cell-based, but I could use view-based if that would work better. Thank you in advance.
class WindowController: NSWindowController, NSTableViewDataSource, NSTableViewDelegate {
#IBOutlet var tableView: NSTableView!
var sports : [String] = ["Basketball","Baseball","Football","Tennis"]
override func windowDidLoad() {
super.windowDidLoad()
tableView.registerForDraggedTypes([NSPasteboard.PasteboardType.fileURL])
tableView.dataSource = self
tableView.delegate = self
}
func numberOfRows(in tableView: NSTableView) -> Int {
return (sports.count)
}
func tableView(_ tableView: NSTableView, objectValueFor tableColumn: NSTableColumn?, row: Int) -> Any? {
var value : Any? = 0
let columnIdentifier : String = (tableColumn?.identifier.rawValue)!
if (columnIdentifier == "Col1"){
value = sports[row]
}
return value
}
In Document.swift windowController is released at the end of showTableView() and the table view looses its data source. Add windowController to the window controllers of the document or hold a strong reference to windowController.
#IBAction func showTableView(_ sender: Any) {
let windowController = WindowController.init(windowNibName:NSNib.Name("WindowController"))
addWindowController(windowController)
windowController.showWindow(nil)
}

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.

UIview kept going at the bottom of the UIViewcontroller

My LoginViewController has a UIView that has 2 UItextfield and 1 UIbutton. The moment the user start writting the UIView should go up and leave space for the keyboard. However my problem is when the keyboard disappear the UIView did not go at its initial position. Can anyone help me please Thank you ... (my code is below)
func keyboardON(sender: NSNotification) {
let info = sender.userInfo!
var keyboardSize = info[UIKeyboardFrameBeginUserInfoKey]!.CGRectValue().size
println("keyboard height \(keyboardSize.height)")
var frame = otherContainerView.frame
println("MainScreen :\(UIScreen.mainScreen().bounds.height)")
frame.origin.y = UIScreen.mainScreen().bounds.height - keyboardSize.height - frame.size.height
otherContainerView.frame = frame
}
func keyboardNotOn(sender: NSNotification) {
let info = sender.userInfo!
var keyboardSize = info[UIKeyboardFrameBeginUserInfoKey]!.CGRectValue().size
println("keyboard height \(keyboardSize.height)")
var frame = otherContainerView.frame
frame.origin.y = UIScreen.mainScreen().bounds.height - frame.size.height
otherContainerView.frame = frame
}
I would suggest that you change this view controller to a UITableViewController with static cells. When using a UITableViewController, the scrolling is automatically handled, thus making your problem not problem at all.
If I understand your initial question correctly, you are trying to create a login screen in a UIViewController, which is fine, but much harder than it would be to simply create a UITableViewController. The image above was made with a UITableViewController. When the text fields are selected, it slides up, and when they are deselected, it moves back to it's initial view. If you switch to a UITableViewController, (even if you place both UITextFields and the button in one cell), you won't need to do any of this programmatically. The storyboard will handle the desired changes.
import UIKit
class LoginTableViewController: UITableViewController {
#IBOutlet weak var usernameTextField : UITextField!
#IBOutlet weak var passwordTextField : UITextField!
#IBAction func login (sender: UIButton) {
//login button
}
override func viewDidLoad() {
super.viewDidLoad()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
//If you choose to use 3 cells in your storyboard (like I've done with my login screen) you will return 3 here. If you choose to put all of the items in one cell, return 1 below.
return 3
}
}