swiftmacosappkitnsoutlineview

How do i create a NSOutline programatically without using interface builder? I think I have implemented every steps for creating this


I am currently working on a macOS AppKit application and trying to implement an NSOutlineView with custom data using NSOutlineViewDataSource and NSOutlineViewDelegate. However, despite implementing the necessary methods, I am facing an issue where the outline view is not populating with data.

Here's a brief overview of my setup:

My Entire Code along with custom implementation:

class OutlineViewController: NSViewController, NSOutlineViewDataSource, NSOutlineViewDelegate {
    // MARK: - Properties
    private var outlineView: NSOutlineView!
    // MARK: - Lifecycle
    override func viewDidLoad() {
        super.viewDidLoad()
        let rootNode = Node(name: "Root Node")
        let childNode1 = Node(name: "Child Node 1")
        let childNode2 = Node(name: "Child Node 2")
        rootNode.addChild(childNode1)
        rootNode.addChild(childNode2)
    }
    override func loadView() {
        view = NSView(frame: NSRect(x: 0, y: 0, width: (NSScreen.main?.frame.width)!, height: (NSScreen.main?.frame.height)!)) // Set some default values

        // Create an outline view
        outlineView = NSOutlineView(frame: NSRect(x: 0, y: 0, width: (NSScreen.main?.frame.width)!, height: (NSScreen.main?.frame.height)!))

        // Register the OutlineCell

        // Set default row height
        outlineView.rowHeight = 50

        // Set the outline view's delegate and data source
        outlineView.delegate = self
        outlineView.dataSource = self

        // Reload the outline view
        outlineView.reloadData()

        let scrollview = NSScrollView(frame: NSRect(x: 0, y: 0, width: 800, height: 600))
        scrollview.documentView = outlineView
        view.addSubview(scrollview)

        NSLayoutConstraint.activate([
            outlineView.topAnchor.constraint(equalTo: scrollview.topAnchor),
            outlineView.bottomAnchor.constraint(equalTo: scrollview.bottomAnchor),
            outlineView.leadingAnchor.constraint(equalTo: scrollview.leadingAnchor),
            outlineView.trailingAnchor.constraint(equalTo: scrollview.trailingAnchor)
        ])
    }

    // MARK: - NSOutlineViewDataSource

    func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any {
        if let item = item as? Node {
            return item.children[index]
        } else {
            return 0
        }
    }

    func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool {
        if let item = item as? Node {
                    return item.children.count > 0
                } else {
                    return false
                }
    }

    func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int {
        if let item = item as? Node {
            return item.children.count
        } else {
            return 0
        }
    }

    // MARK: - NSOutlineViewDelegate

    func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? {
        guard let cell = outlineView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: TableCellView.identifier), owner: self) as? TableCellView else {
            let cell =  TableCellView()
            cell.profileImage.image = NSImage(named: "boy")
            cell.identifier = NSUserInterfaceItemIdentifier(TableCellView.identifier)
            cell.label.stringValue = "BOY"
            return cell
        }
        cell.profileImage.image = NSImage(named: "girl")
        cell.label.stringValue = "GIRL"
        return cell

    }


}
}

class Node {

    let name: String
    var children: [Node] = []

    init(name: String) {
        self.name = name
    }

    func addChild(_ child: Node) {
        children.append(child)
    }
}

class TableCellView: NSTableCellView {
    
    static var identifier =  "TableCell"
    
    var profileImage = NSImageView()
    var label = NSTextField(labelWithString: "")
    
    override init(frame frameRect: NSRect) {
        
        super.init(frame: frameRect)
        addSubview(profileImage)
        addSubview(label)
        profileImage.wantsLayer = true
        profileImage.animates = true
        profileImage.layer?.cornerRadius = 45
        label.isEditable = false
        label.alignment = .center
        label.font = .boldSystemFont(ofSize: 20)
        profileImage.translatesAutoresizingMaskIntoConstraints = false
        
        NSLayoutConstraint.activate([
            profileImage.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 50),
            profileImage.widthAnchor.constraint(equalToConstant: 90),
            profileImage.topAnchor.constraint(equalTo: topAnchor, constant: 20),
            profileImage.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -20),
        ])
        
        label.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            label.leadingAnchor.constraint(equalTo: profileImage.trailingAnchor),
            label.topAnchor.constraint(equalTo: topAnchor, constant: 55),
            label.bottomAnchor.constraint(equalTo: bottomAnchor),
            label.trailingAnchor.constraint(equalTo: trailingAnchor)
        ])
        
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

I am only getting empty scroll view.


Solution

  • There are several issues:

    1. The outline view doesn't have any columns.
    outlineView.addTableColumn(NSTableColumn(identifier: NSUserInterfaceItemIdentifier(rawValue: TableCellView.identifier)))
    

    and at the end of loadView():

    outlineView.sizeLastColumnToFit()
    
    1. You're not using rootNode anywhere. In the data source methods, if item is nil then the item is the root object.
    func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any {
        if let item = item as? Node {
            return item.children[index]
        } else {
            return rootNode.children[index]
        }
    }
    
    func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool {
        if let item = item as? Node {
            return item.children.count > 0
        } else {
            return rootNode.children.count > 0
        }
    }
    
    func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int {
        if let item = item as? Node {
            return item.children.count
        } else {
            return rootNode.children.count
        }
    }
    
    1. viewDidLoad is called after loadView so reloadData() is called before rootNode is populated. Call reloadData() in viewDidLoad().

    2. The contraints of TableCellView need some work.