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!
    }
}
}