NSTableView delegate's viewFor method not triggering (cells aren't displayed nor editable)

91 Views Asked by At

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
    }

}
0

There are 0 best solutions below