Programmatically adding a NSTableView to a NSStackView

574 views Asked by At

I was wondering about programmatically adding a NSTableView inside a NSStackView using Swift 3/MacOS Sierra.

The idea would be to have say 2 NSTextFields aligned via the centerY axis in the .leading gravity space, then a tableview in the .center gravity space, then 2 more NSTextFields aligned via the centerY axis in the .trailing gravity space. The stack view would span the width of the NSView -- like a header.

Is this a good idea or should I avoid doing this? It has been very difficult to get it to look correct -- the table always has too large of a width despite adding constraints to try to pin it to a fixed width.

Any insight would be appreciated. I'm new to programming MacOS.

Thanks,

Here is the output in Interface Builder: output of the headerview

Here is the code of the NSView I'm using: The view controller is elsewhere but I'm not really having problems with the view controller -- it's displaying the data in the table correctly. It's just the sizing/positioning of the tableview (which I'm trying to do in the NSView via the NSStackView) is always wrong. It should have a width of 650 but instead has a width of 907 and I get the same error all the time in the debug console:

2017-09-12 17:43:36.041062-0500 RaceProgram[795:36958] [Layout] Detected missing constraints for < RacingProgram.RaceImportViewHeader: 0x6000001ccd50 >. It cannot be placed because there are not enough constraints to fully define the size and origin. Add the missing constraints, or set translatesAutoresizingMaskIntoConstraints=YES and constraints will be generated for you. If this view is laid out manually on macOS 10.12 and later, you may choose to not call [super layout] from your override. Set a breakpoint on DETECTED_MISSING_CONSTRAINTS to debug. This error will only be logged once.

import Cocoa

@IBDesignable
class RaceImportViewHeader: NSView {

// MARK: Properties
private var raceQualificationsTableView:NSTableView

private var raceImportHeaderStackView:NSStackView

private var raceNumberTitle: NSTextField
private var raceNumberValue: NSTextField

public var raceQualificationsTableRowHeight: CGFloat            

@IBInspectable var borderColor:NSColor = .black
@IBInspectable var backgroundColor:NSColor = .lightGray

enum InitMethod {
    case Coder(NSCoder)
    case Frame(CGRect)
}

override convenience init(frame: CGRect) {
    self.init(.Frame(frame))!
}

required convenience init?(coder: NSCoder) {
    self.init(.Coder(coder))
}

private init?(_ initMethod: InitMethod) {
    // Group together the initializers for this view class

    raceQualificationsTableView = NSTableView()

    raceImportHeaderStackView = NSStackView()

    raceNumberTitle = NSTextField()
    raceNumberValue = NSTextField()

    raceQualificationsTableRowHeight = 17.0         // Initialize the row height for raceQualifications

    switch initMethod {
    case let .Coder(coder): super.init(coder: coder)
    case let .Frame(frame): super.init(frame: frame)
    }

    self.translatesAutoresizingMaskIntoConstraints = false

    drawUI()
}


override func draw(_ dirtyRect: NSRect) {
    super.draw(dirtyRect)

    let viewSize: NSRect = self.frame
    let newRect = NSRect(x: 0, y: 0, width: viewSize.width, height: viewSize.height)


    // Outline the Header --> Only for layout debug purposes
    let path = NSBezierPath(rect: newRect)
    backgroundColor.setFill()
    path.fill()
    borderColor.setStroke()         // Set the stroke color
    path.stroke()                   // Fill the stroke or border of the rectangle

}

// MARK: UI Construction

func drawUI() {
    let viewFrame = self.frame       // with respect to the super class
    let viewBounds = self.bounds     // with respect to the view

    // MARK: Race Number Setup

    func addRaceNumberTitle(startingPositionX: CGFloat) {
        // This configures label for race Number

        let width:CGFloat = 60.0                    //Arbitrary at the moment
        let height:CGFloat = 40.0
        let leftPadding:CGFloat = 2.5              // The super view (frame)is the NSView in this case
        let topPadding:CGFloat = (viewBounds.height - height)/2
        let raceNumberTitleNSRect = NSRect(x: leftPadding + startingPositionX, y: viewBounds.height - height - topPadding, width: width, height: height)

        //Swift.print("The raceNumberTitleNSRect title NSRect is \(raceNumberTitleNSRect)")

        raceNumberTitle = NSTextField(frame: raceNumberTitleNSRect)
        raceNumberTitle.stringValue = "Race\nNumber"
        raceNumberTitle.maximumNumberOfLines = 2
        raceNumberTitle.isEditable = false
        raceNumberTitle.isBordered = false
        raceNumberTitle.alignment = .center
        raceNumberTitle.backgroundColor = .clear
        raceNumberTitle.sizeToFit()

        let updatedHeight = raceNumberTitle.frame.height
        let newUpdatedPadding = (viewBounds.height - updatedHeight) / 2
        let oldOriginX = raceNumberTitle.frame.origin.x
        let newOriginY = viewBounds.height - updatedHeight - newUpdatedPadding

        let newOrigin = NSPoint(x: oldOriginX, y: newOriginY)
        raceNumberTitle.setFrameOrigin(newOrigin)

        //addSubview(raceNumberTitle)      // Add to view
        raceImportHeaderStackView.addView(raceNumberTitle, in: .leading)
    }

    func addRaceNumberValue(startingPositionX: CGFloat) {
        // This configures value label for race number

        let width:CGFloat = 20.0                    //Arbitrary at the moment
        let height:CGFloat = 40.0
        let leftPadding:CGFloat = 5.0               // The super view (frame)is the NSView in this case
        let topPadding:CGFloat = (viewBounds.height - height)/2
        let raceNumberInRect = NSRect(x: startingPositionX + leftPadding, y: viewBounds.height - height - topPadding, width: width, height: height)

        Swift.print("The raceNumberInRect title NSRect is \(raceNumberInRect)")

        raceNumberValue = NSTextField(frame: raceNumberInRect)
        raceNumberValue.identifier = "raceNumber"
        raceNumberValue.placeholderString = "1"
        raceNumberValue.font = NSFont(name: "Impact", size: 20.0)
        raceNumberValue.maximumNumberOfLines = 1
        raceNumberValue.isEditable = false
        raceNumberValue.isBordered = true
        raceNumberValue.alignment = .center
        raceNumberValue.backgroundColor = .clear
        raceNumberValue.sizeToFit()


        let updatedHeight = raceNumberValue.frame.height
        let oldOriginX = raceNumberValue.frame.origin.x
        let newUpdatedPadding = (viewBounds.height - updatedHeight) / 2
        let newOriginY = viewBounds.height - updatedHeight - newUpdatedPadding

        let newOrigin = NSPoint(x: oldOriginX, y: newOriginY)
        raceNumberValue.setFrameOrigin(newOrigin)

        //addSubview(raceNumberValue)      // Add to view
        raceImportHeaderStackView.addView(raceNumberValue, in: .leading)
    }


    // MARK: Race Qualifications Table Setup

    func addRaceQualificationsTable(startingPositionX: CGFloat) {

        // Padding variables
        let leftPadding:CGFloat = 5.0
        let topPadding:CGFloat = 5.0

        // Table Properties
        let width:CGFloat = 650.0
        let height:CGFloat = 40

        let tableRect = CGRect(x: startingPositionX + leftPadding, y: viewBounds.height - height - topPadding, width: width, height: height)

        //let insetForTableView:CGFloat = 1.0
        //let scrollRect = CGRect(x: tableRect.origin.x-insetForTableView, y: tableRect.origin.y-insetForTableView, width: tableRect.width+2*insetForTableView, height: tableRect.height+2*insetForTableView)

        let tableNSSize = NSSize(width: tableRect.width, height: tableRect.height)
        let scrollNSRect = NSScrollView.frameSize(forContentSize: tableNSSize, horizontalScrollerClass: nil, verticalScrollerClass: nil, borderType: .bezelBorder, controlSize: .regular, scrollerStyle: .legacy)

        Swift.print("tableRect \(tableRect)")
        Swift.print("scrollNSRect \(scrollNSRect)")
        //Swift.print("scrollRect \(scrollRect)")

        let scrollViewOrigin:CGPoint = tableRect.origin
        let scrollViewNSSize:CGSize = scrollNSRect
        let scrollRect = NSRect(origin: scrollViewOrigin, size: scrollViewNSSize)

        Swift.print("scrollRect \(scrollRect)")

        let tableScrollView = NSScrollView(frame: scrollRect)
        raceQualificationsTableView = NSTableView(frame: tableRect)
        raceQualificationsTableView.identifier = "raceQualificationsTable"          // Setup identifier
        raceQualificationsTableView.rowHeight = 20.0

        Swift.print("instrinic size \(raceQualificationsTableView.intrinsicContentSize)")

        //Swift.print("tableScrollView contentsize \(tableScrollView.contentSize)")

        tableScrollView.documentView = raceQualificationsTableView
        tableScrollView.autoresizingMask = .viewNotSizable
        Swift.print("tableScroll content size \(tableScrollView.contentSize)")
        //self.addSubview(tableScrollView)

        raceImportHeaderStackView.addView(tableScrollView, in: .center)


    }

    func configureRaceQualificationsTable(showRaceNumberCol: Bool, showRaceCodeCol: Bool) {

        let headerAlignment = NSTextAlignment.center    // Easy way to change justification of headers

        // MARK: Race Number Column Options
        let raceNumberColumn = NSTableColumn(identifier: "raceNumberCol")
        raceNumberColumn.title = "Race"
        raceNumberColumn.minWidth = 40.0
        raceNumberColumn.width = 40.0
        raceNumberColumn.headerToolTip = "Race Number from the Imported Card"
        raceNumberColumn.headerCell.alignment = headerAlignment
        // Note: Word Race is always going to be wider than the race number value
        // So size to Fit is appropriate here.
        raceNumberColumn.sizeToFit()

        if showRaceNumberCol {
            // Option of not adding this to the table
            raceQualificationsTableView.addTableColumn(raceNumberColumn)
        }


        // MARK: Driver Column Options
        let breedColumn = NSTableColumn(identifier: "driverCol")
        driverColumn.title = "Driver"
        driverColumn.minWidth = 10
        driverColumn.headerToolTip = "Driver information"
        driverColumn.headerCell.alignment = headerAlignment
        driverColumn.sizeToFit()
        raceQualificationsTableView.addTableColumn(driverColumn)

        // MARK: Race Code Column Options
        let raceTypeCodeColumn = NSTableColumn(identifier: "raceTypeCodeCol")
        raceTypeCodeColumn.title = "Race Code"
        raceTypeCodeColumn.minWidth = 40
        raceTypeCodeColumn.headerToolTip = "Race Classification Code"
        raceTypeCodeColumn.headerCell.alignment = headerAlignment
        raceTypeCodeColumn.sizeToFit()

        if showRaceCodeCol {
            // Option of not adding to the table
            raceQualificationsTableView.addTableColumn(raceTypeCodeColumn)
        }

        // MARK: Race Type Code Description Options
        let raceTypeCodeDescColumn = NSTableColumn(identifier: "raceTypeCodeDescCol")
        raceTypeCodeDescColumn.title = "Race Desc"
        raceTypeCodeDescColumn.minWidth = 50
        raceTypeCodeDescColumn.width = 100
        raceTypeCodeDescColumn.headerToolTip = "Race Classification Full Description"
        raceTypeCodeDescColumn.headerCell.alignment = headerAlignment

        raceQualificationsTableView.addTableColumn(raceTypeCodeDescColumn)

        // MARK: Race Restriction Column Options
        let raceRestrictionColumn = NSTableColumn(identifier: "raceRestrictionCol")
        raceRestrictionColumn.title = "Restrictions"
        raceRestrictionColumn.minWidth = 50
        raceRestrictionColumn.width = 80
        raceRestrictionColumn.headerToolTip = "Race Restrictions"
        raceRestrictionColumn.headerCell.alignment = headerAlignment

        raceQualificationsTableView.addTableColumn(raceRestrictionColumn)

        // MARK: Sex Restriction Column Options
        let sexRestrictionColumn = NSTableColumn(identifier: "sexRestrictionCol")
        sexRestrictionColumn.title = "Sex"
        sexRestrictionColumn.minWidth = 100
        sexRestrictionColumn.width = 100
        sexRestrictionColumn.headerToolTip = "Sex Restrictions"
        sexRestrictionColumn.headerCell.alignment = headerAlignment

        raceQualificationsTableView.addTableColumn(sexRestrictionColumn)

        // MARK: Age Restriction Column Options
        let ageRestrictionColumn = NSTableColumn(identifier: "ageRestrictionCol")
        ageRestrictionColumn.title = "Age"
        ageRestrictionColumn.minWidth = 100
        ageRestrictionColumn.width = 100
        ageRestrictionColumn.headerToolTip = "Age Restrictions"
        ageRestrictionColumn.headerCell.alignment = headerAlignment

        raceQualificationsTableView.addTableColumn(ageRestrictionColumn)

        // MARK: Division Column Options
        let divisionColumn = NSTableColumn(identifier: "divisionCol")
        divisionColumn.title = "Division"
        divisionColumn.minWidth = 50

        let minDivisionColumnWidth = raceQualificationsTableView.frame.width - raceNumberColumn.width - driverColumn.width - raceTypeCodeColumn.width - raceTypeCodeDescColumn.width - raceRestrictionColumn.width - sexRestrictionColumn.width - ageRestrictionColumn.width

        // Calculate the available room for the division column
        if (showRaceCodeCol && showRaceNumberCol) {
            // This is the minimum case
            // No idea why we need the 25.0 manual adjustment
            divisionColumn.width = minDivisionColumnWidth - 25.0
        } else if (showRaceCodeCol && !showRaceNumberCol) {
            // Add back race type code
            // No idea why we need to manually adjust 53.5
            divisionColumn.width = minDivisionColumnWidth + raceTypeCodeColumn.width - 53.5
        } else if (!showRaceCodeCol && showRaceNumberCol) {
            // Add back race number col
            divisionColumn.width = minDivisionColumnWidth + raceNumberColumn.width
        } else {
            // Else it's the maximum space
            // This code was making the frame too large -- it was increasing the
            // the frame size of the column to 670.0  I put a manual reduction of
            // 20 to keep the frame size the same.  Not sure where this 20 is coming from.
            divisionColumn.width = minDivisionColumnWidth + raceNumberColumn.width + raceTypeCodeColumn.width - 20.0
        }

        //Swift.print("The division column width is \(divisionColumn.width)")

        divisionColumn.headerToolTip = "Division -- Unknown what this means"
        divisionColumn.headerCell.alignment = headerAlignment

        raceQualificationsTableView.addTableColumn(divisionColumn)

        //Swift.print("raceQualificationsTableView.frame.width is \( raceQualificationsTableView.frame.width)")
    }

    // MARK: Race Distance Surface Course Setup

    func addRaceDistanceSurfaceCourseTable(startingPositionX: CGFloat) {
        // Table Properties
        let width:CGFloat = 250.0
        let height:CGFloat = 40.0

        // Padding variables
        let leftPadding:CGFloat = 5.0

        let topPosition:CGFloat = (viewBounds.height - ((viewBounds.height - height)/2) - height)

        let tableRect = CGRect(x: leftPadding + startingPositionX, y: topPosition, width: width, height: height)

        let tableScrollView = NSScrollView(frame: tableRect)
        raceDistanceSurfaceCourseTableView = NSTableView(frame: tableRect)
        raceDistanceSurfaceCourseTableView.identifier = "raceDistanceSurfaceCourseTable"          // Setup identifier
        //raceDistanceSurfaceCourseTableView.rowHeight = 20.0
        raceDistanceSurfaceCourseTableView.intercellSpacing = NSSize(width: 1.0, height: 1.0)
        raceDistanceSurfaceCourseTableView.headerView = ImportRaceTableHeaders()

        tableScrollView.documentView = raceDistanceSurfaceCourseTableView
        //tableScrollView.hasVerticalScroller = false
        //tableScrollView.verticalScroller = nil          // Turn off vertical scrolling
        //tableScrollView.verticalScrollElasticity = .none

        //raceDistanceSurfaceCourseTableView = NSTableViewHeader

        //self.addSubview(tableScrollView)
        raceImportHeaderStackView.addView(raceDistanceSurfaceCourseTableView, in: .center)

    }



    // MARK: Construct the fields:

    //configureHeaderView()
    configureStackView()
    addRaceNumberTitle(startingPositionX: 0.0)              // Add the race number title
    addRaceNumberValue(startingPositionX: raceNumberTitle.frame.origin.x + raceNumberTitle.frame.width)   //Add the Race Number value text field

    addRaceQualificationsTable(startingPositionX: raceNumberValue.frame.origin.x + raceNumberValue.frame.width)
    configureRaceQualificationsTable(showRaceNumberCol: false, showRaceCodeCol: false)

}

// MARK: TableView Functions

func reloadTableViewData(identifier: String) {

    Swift.print("Manual reload of data for identifier \(identifier)")

    switch identifier {

        case "raceQualificationsTable":
            raceQualificationsTableView.reloadData()
        case "raceDistanceSurfaceCourseTable":
            raceDistanceSurfaceCourseTableView.reloadData()
        default:
            break
    }
}


// MARK: Delegate/DataSources Outlets for TableViews

// Race Qualification Table (the header table)
@IBOutlet weak var raceQualificationsDelegate: NSTableViewDelegate? {
    get {
        return raceQualificationsTableView.delegate
    }
    set {
        raceQualificationsTableView.delegate = newValue
    }
}

@IBOutlet weak var raceQualificationsDataSource: NSTableViewDataSource? {
    get {
        return raceQualificationsTableView.dataSource
    }
    set {
        raceQualificationsTableView.dataSource = newValue
    }
}

// Race Distance Surface Course
@IBOutlet weak var raceDistanceSurfaceCourseDelegate: NSTableViewDelegate? {
    get {
        return raceDistanceSurfaceCourseTableView.delegate
    }
    set {
        raceDistanceSurfaceCourseTableView.delegate = newValue
    }
}

@IBOutlet weak var raceDistanceSurfaceCourseDataSource: NSTableViewDataSource? {
    get {
        return raceDistanceSurfaceCourseTableView.dataSource
    }
    set {
        raceDistanceSurfaceCourseTableView.dataSource = newValue
    }
}

// MARK: Label Outlets
@IBOutlet var raceNumber:String? {
    get {
        return raceNumberValue.stringValue
    }
    set {
        raceNumberValue.stringValue = newValue!
    }
}

}

0

There are 0 answers