Skip to content

Instantly share code, notes, and snippets.

@Lessica
Last active March 11, 2024 13:26
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Lessica/176c2314336fc861398de1e1045aa368 to your computer and use it in GitHub Desktop.
Save Lessica/176c2314336fc861398de1e1045aa368 to your computer and use it in GitHub Desktop.
A Checkbox Cell in NSTableView Column Header
//
// CheckboxHeaderCell.swift
//
// Created by Rachel on 2021/4/26.
// Original: https://stackoverflow.com/questions/11961869/checkbox-in-nstableview-column-header
//
import Cocoa
class CheckboxCell: NSButtonCell {
var alternateState: NSControl.StateValue = .off {
didSet {
super.state = alternateState
}
}
// Ignores the default behavior
override var state: NSControl.StateValue {
get { alternateState }
set { }
}
}
class CheckboxHeaderCell: NSTableHeaderCell {
private lazy var innerCell: CheckboxCell = {
let cell = CheckboxCell()
cell.title = ""
cell.setButtonType(.switch)
cell.type = .nullCellType
cell.isBordered = false
cell.imagePosition = .imageOnly
cell.alignment = .center
cell.objectValue = NSNumber(booleanLiteral: false)
cell.controlSize = .regular
cell.font = NSFont.systemFont(ofSize: NSFont.systemFontSize(for: .small))
cell.allowsMixedState = true
return cell
}()
// Hide the default "Field" text
override var textColor: NSColor? {
get { .clear }
set { }
}
override var title: String {
get { innerCell.title }
set { innerCell.title = newValue }
}
override var image: NSImage? {
get { innerCell.image }
set { innerCell.image = newValue }
}
// We should not override `state` of this class.
// Instead, a property named `alternateState` is better for accessing its innerCell's state.
var alternateState: NSControl.StateValue {
get { innerCell.alternateState }
set { innerCell.alternateState = newValue }
}
func toggleAlternateState() -> NSControl.StateValue {
innerCell.alternateState = (alternateState != .on ? .on : .off)
return innerCell.alternateState
}
// Override the original -drawWithFrame:inView: will also remove the background or border of the original cell, which is not recommended.
override func drawInterior(withFrame cellFrame: NSRect, in controlView: NSView) {
super.drawInterior(withFrame: cellFrame, in: controlView)
let centeredRect = CGRect(
x: cellFrame.midX - innerCell.cellSize.width / 2.0,
y: cellFrame.midY - innerCell.cellSize.height / 2.0,
width: innerCell.cellSize.width,
height: innerCell.cellSize.height
)
innerCell.draw(withFrame: centeredRect, in: controlView)
}
}
@Lessica
Copy link
Author

Lessica commented Apr 27, 2021

Usage

Setup

@IBOutlet weak var tableColumnChecked: NSTableColumn!
override func viewDidLoad() {
    super.viewDidLoad()
    tableColumnChecked.headerCell = CheckboxHeaderCell()
}

Send Action

private extension NSUserInterfaceItemIdentifier {
    // the checkbox column
    static let columnChecked = NSUserInterfaceItemIdentifier("col-checked")
}

func setupAlternateState(_ state: NSControl.StateValue) {
    // modify data source
}

func tableView(_ tableView: NSTableView, didClick tableColumn: NSTableColumn) {
    if tableColumn == tableColumnChecked {
        if let headerCell = tableColumn.headerCell as? CheckboxHeaderCell {
            // apply state changes to its data source
            setupAlternateState(headerCell.toggleAlternateState())
            // reload display, select or deselect all checkboxes
            tableView.reloadData(
                forRowIndexes: IndexSet(integersIn: 0..<tableView.numberOfRows),
                columnIndexes: IndexSet(integer: tableView.column(withIdentifier: .columnChecked))
            )
        }
    }
}

Re-draw

if let headerCell = tableColumnChecked.headerCell as? CheckboxHeaderCell {
    headerCell.alternateState = .on  // .off or .mixed
    tableView.headerView?.needsDisplay = true
}

@Jaycyn
Copy link

Jaycyn commented Mar 2, 2023

This is crashing for me

Copy and pasted the code; have a tableVIew with two columns. XCode 14.2 macOS 12.6.3

Crashes here

override var image: NSImage? {
   get { innerCell.image }   //<- Crash with Thread 1: EXC_BAD_ACCESS (code=1, address=0x20)
   set { innerCell.image = newValue }
}

Any suggestions?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment