Update: The solution is now in the comments
I have a problem with NSTableView not displaying cells in my macOS app. I have implemented the NSTableViewDataSource and NSTableViewDelegate protocols, but the tableView(_:viewFor:row:) method is not being triggered.
The classes in question are TableDelegate and ViewController
To test:
Click the bottom left "+" button to add rows to the table. You have to click a row to see it, unfortunately.
Minimal reproducible example
import SwiftUI
@main
struct MinimalReproducibleExampleApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
import Cocoa
import SwiftUI
struct ContentView: View {
var body: some View {
ViewControllerWrapper()
}
}
struct ViewControllerWrapper: NSViewControllerRepresentable {
func makeNSViewController(context: Context) -> ViewController {
ViewController(listType: "rand")
}
func updateNSViewController(_ nsViewController: ViewController, context: Context) {
// You can add any logic you need here to update the view controller as needed.
}
}
class RandList {
//let databaseManager = DatabaseManager()
var words: [String]
private(set) var listType: String
private(set) var listName: String
var isUserListLoaded: Bool = false
var savedListNames: [String]
init(listType: String) {
self.listType = listType
self.listName = ""
self.words = []
self.savedListNames = [""]
self.getCurListAndWords()
}
func getCurListAndWords(){
let currentListName = ""
self.listName = currentListName
//self.words = databaseManager.selectWords(listName: currentListName, listType: listType, fromCurrentList: true)
}
func saveList(newListName: String) {
print("in saveList of RandList for newListName: " + newListName + ", listType: " + listType)
if true {
print("list already exists, invoking confirmOverwrite")
let result = true //confirmOverwrite(listName: newListName)
if result {
// databaseManager.deleteList(table: listType + "Lists", listName: newListName, listType: listType)
// databaseManager.insertList(table: listType + "Lists", listName: newListName, listType: listType, words: words)
// databaseManager.deleteList(table: "cur_\(listType)list", listName: listName, listType: listType)
// databaseManager.insertList(table: "cur_\(listType)list", listName: newListName, listType: listType, words: words)
self.listName = newListName
}
} else {
print("list doesn't exist, so inserting for both table types")
// databaseManager.insertList(table: listType + "Lists", listName: newListName, listType: listType, words: words)
// databaseManager.deleteList(table: "cur_\(listType)list", listName: listName, listType: listType)
// databaseManager.insertList(table: "cur_\(listType)list", listName: newListName, listType: listType, words: words)
self.listName = newListName
}
}
}
class TableDelegate: NSObject, NSTableViewDelegate, NSTableViewDataSource {
var tableView: NSTableView
let randList: RandList
private var dataSource: [String] {
get { return randList.words }
set { randList.words = newValue }
}
init(randList: RandList, tableView:NSTableView) {
self.randList = randList
self.tableView = tableView
super.init()
}
func setDataSource(){ // invoked from ViewController
dataSource = randList.words
}
func addWord() {
//print("in TableDelegate addWord()")
randList.words.append("")
tableView.reloadData()
print(tableView.numberOfRows.description + " -- tableView.numberOfRows after appending")
//tableView.insertRows(at: IndexSet(integer: newRowIndex), withAnimation: .slideDown)
}
func numberOfRows(in tableView: NSTableView) -> Int {
print("in numberOfRows")
print(dataSource.count)
return dataSource.count
}
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
print("in viewFor")
let cellView = NSTableCellView()
let textField = NSTextField()
textField.isEditable = true
textField.isBordered = true
textField.backgroundColor = NSColor.clear
textField.font = NSFont.systemFont(ofSize: 16)
cellView.addSubview(textField)
textField.translatesAutoresizingMaskIntoConstraints = false
textField.leadingAnchor.constraint(equalTo: cellView.leadingAnchor).isActive = true
textField.trailingAnchor.constraint(equalTo: cellView.trailingAnchor).isActive = true
textField.topAnchor.constraint(equalTo: cellView.topAnchor).isActive = true
textField.bottomAnchor.constraint(equalTo: cellView.bottomAnchor).isActive = true
let word = randList.words[row]
cellView.textField?.stringValue = word
return cellView
}
func tableView(_ tableView: NSTableView, backgroundColorForRow row: Int) -> NSColor? {
if row % 2 == 0 {
return NSColor(calibratedWhite: 0.95, alpha: 1)
} else {
return NSColor(calibratedWhite: 1, alpha: 1)
}
}
}
class ViewController: NSViewController, NSControlTextEditingDelegate {
var randList: RandList
var tableView = NSTableView()
private let scrollView = NSScrollView()
let titleLabel = NSTextField()
let addButton = NSButton(title: "+", target: nil, action: nil)
let saveButton = NSButton(title: "Save", target: nil, action: nil)
var closeButton: NSButton!
let deleteButton = NSButton(title: "Delete", target: nil, action: nil)
let listPicker = NSPopUpButton()
var tableDelegate: TableDelegate
init(listType: String) {
self.randList = RandList(listType: listType)
self.tableDelegate = TableDelegate(randList: randList, tableView: tableView)
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func loadView() {
self.view = NSView()
}
override func viewDidLoad() {
super.viewDidLoad()
setupTitleLabel()
setupCloseButton()
setupAddButton()
setupSaveButton()
setupDeleteButton()
setupListPicker()
setupTableView()
let listWidth: CGFloat = randList.listType == "rand" ? 500 : 275
let listHeight: CGFloat = 325
// Ensure the view controller starts with a minimum size
view.setFrameSize(NSSize(width: listWidth, height: listHeight))
// Constraints for the table view
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.widthAnchor.constraint(equalToConstant: listWidth).isActive = true
tableView.heightAnchor.constraint(equalToConstant: listHeight).isActive = true
tableView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
tableView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
// Constraints for the close button
closeButton.translatesAutoresizingMaskIntoConstraints = false
closeButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20).isActive = true
closeButton.topAnchor.constraint(equalTo: view.topAnchor, constant: 20).isActive = true
}
func setupCloseButton() {
closeButton = NSButton(frame: NSRect(x: view.frame.width - 50, y: view.frame.height - 40, width: 50, height: 50))
closeButton.bezelStyle = .roundRect
closeButton.title = "x"
closeButton.font = NSFont.systemFont(ofSize: 20)
closeButton.target = self
closeButton.action = #selector(closeButtonPressed)
print("closeButton.action: " + (closeButton.action?.description ?? ""))
view.addSubview(closeButton)
}
@objc func closeButtonPressed() {
self.view.removeFromSuperview()
}
private func setupTableView(){
tableView.delegate = self.tableDelegate
tableView.dataSource = tableDelegate
tableView.reloadData()
scrollView.hasVerticalScroller = true
scrollView.autoresizesSubviews = false
scrollView.documentView = tableView
tableView.headerView = nil
view.addSubview(scrollView)
scrollView.translatesAutoresizingMaskIntoConstraints = false
view.translatesAutoresizingMaskIntoConstraints = false
let widthConstraint = view.widthAnchor.constraint(greaterThanOrEqualToConstant: 300)
widthConstraint.priority = .defaultHigh
widthConstraint.isActive = true
let heightConstraint = view.heightAnchor.constraint(greaterThanOrEqualToConstant: 500)
heightConstraint.priority = .defaultHigh
heightConstraint.isActive = true
scrollView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 20).isActive = true
scrollView.bottomAnchor.constraint(equalTo: saveButton.topAnchor, constant: -20).isActive = true
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20).isActive = true
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20).isActive = true
}
private func setupTitleLabel() {
titleLabel.stringValue = "\(randList.listType)list 1"
titleLabel.isEditable = true
titleLabel.isBordered = true
titleLabel.backgroundColor = NSColor.clear
titleLabel.isSelectable = false
titleLabel.font = NSFont.systemFont(ofSize: 32, weight: .bold)
//titleLabel.delegate = self
view.addSubview(titleLabel)
titleLabel.translatesAutoresizingMaskIntoConstraints = false
titleLabel.topAnchor.constraint(equalTo: view.topAnchor, constant: 20).isActive = true
titleLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
}
private func setupAddButton() {
addButton.bezelStyle = .rounded
view.addSubview(addButton)
addButton.target = self
addButton.action = #selector(addWord)
addButton.translatesAutoresizingMaskIntoConstraints = false
addButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -20).isActive = true
addButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20).isActive = true
}
@objc private func addWord() {
// Add a new word to the data source
tableDelegate.addWord()
// Set the Save button as enabled
saveButton.isEnabled = true
// Enable scrolling
tableView.enclosingScrollView?.hasVerticalScroller = true
}
private func setupSaveButton() {
saveButton.bezelStyle = .rounded
saveButton.isEnabled = false
view.addSubview(saveButton)
saveButton.translatesAutoresizingMaskIntoConstraints = false
saveButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -20).isActive = true
saveButton.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
}
private func setupDeleteButton() {
//let deleteButton = NSButton(title: "Delete", target: self, action: #selector(deleteList))
deleteButton.target = self
deleteButton.action = #selector(deleteList)
deleteButton.isEnabled = randList.isUserListLoaded
view.addSubview(deleteButton)
deleteButton.translatesAutoresizingMaskIntoConstraints = false
deleteButton.topAnchor.constraint(equalTo: saveButton.topAnchor).isActive = true
deleteButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -10).isActive = true
}
private func setupListPicker() {
let picker = listPicker //NSPopUpButton(frame: .zero, pullsDown: false)
picker.pullsDown = false
picker.addItems(withTitles: randList.savedListNames)
picker.isEnabled = !randList.savedListNames.isEmpty
picker.target = self
picker.action = #selector(loadList)
view.addSubview(picker)
picker.translatesAutoresizingMaskIntoConstraints = false
picker.topAnchor.constraint(equalTo: saveButton.topAnchor).isActive = true
picker.trailingAnchor.constraint(equalTo: deleteButton.leadingAnchor, constant: -10).isActive = true
// if the list exists as a user-saved list, set the picker to that list.
if true{
listPicker.selectItem(withTitle: randList.listName)
// triggers loadList
}
else{
// otherwise, the picker shouldn't have a selection, but loadList should be invoked, which will just do:
// `dataSource = randList.words` and `tableView.reloadData()` if the picker selection == ""
loadList()
}
}
@objc private func deleteList() {
let listType = randList.listType
deleteButton.isEnabled = false
listPicker.removeAllItems()
//randList.savedListNames = randList.databaseManager.getAllUserLists(listType:randList.listType)
listPicker.addItems(withTitles: randList.savedListNames)
listPicker.isEnabled = !randList.savedListNames.isEmpty
//dataSource.removeAll()
tableDelegate.randList.words.removeAll()
if let firstListName = randList.savedListNames.first {
listPicker.selectItem(withTitle: firstListName)
loadList()
// tableView.reloadData() called in loadList()
}
else{
tableView.reloadData()
}
}
@objc private func loadList() {
let selectedList = listPicker.titleOfSelectedItem ?? ""
if selectedList != ""{
// load all the words from the selectedList and insert as curList, then just set listName and words to randList
let listType = randList.listType
var words = [""]
//randList.databaseManager.insertList(table:"cur_", listName: selectedList, listType: listType, words: words)
randList.getCurListAndWords()
}
print("set dataSource to randList.words in ViewController's loadList function")
tableDelegate.setDataSource()
tableView.reloadData()
}
@objc private func saveList() {
randList.saveList(newListName: randList.listName)
listPicker.addItem(withTitle: randList.listName)
saveButton.isEnabled = false
}
}