4 years ago

Add and remove footer using NSBox

If you use macOS, you have probably realized many apps have the following UI component on their settings:

Screenshots that shows the UI controller that many apps use to add or remove items from a list

I had to add one of those to the settings view of Angle, and then I realized that it’s not a pre-defined component that you can drag & drop and use. Does that mean we have to implement a custom view for it? You are right! And that’s what I ended up doing.

It took me several iterations until I got it right. Since I’m sure I’m not the first one coming across this need, I’ll leave the code snippet here:

import Foundation
import AppKit

class AddRemoveFooter: NSBox {

    // MARK: - Attributes

    fileprivate var addButton: NSButton!
    fileprivate var removeButton: NSButton!

    // MARK: - Init

    override init(frame frameRect: NSRect) {
        super.init(frame: frameRect)
        setup()
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setup()
    }

    // MARK: - Internal

    func setAddAction(_ action: Selector?, target: AnyObject?) {
        addButton.action = action
        addButton.target = target
    }

    func setRemoveAction(_ action: Selector?, target: AnyObject?) {
        removeButton.action = action
        removeButton.target = target
    }

    // MARK: - Fileprivate

    fileprivate func setup() {
        setupStyle()
        setupButtons()
    }

    fileprivate func setupButtons() {
        contentView = NSView()
        contentView!.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            contentView!.leadingAnchor.constraint(equalTo: self.leadingAnchor),
            contentView!.topAnchor.constraint(equalTo: self.topAnchor),
            contentView!.bottomAnchor.constraint(equalTo: self.bottomAnchor),
            contentView!.trailingAnchor.constraint(equalTo: self.trailingAnchor),
        ])

        addButton = NSButton(title: "+", target: nil, action: nil)
        addButton.bezelStyle = .shadowlessSquare
        addButton.translatesAutoresizingMaskIntoConstraints = false

        removeButton = NSButton(title: "﹣", target: nil, action: nil)
        removeButton.bezelStyle = .shadowlessSquare
        removeButton.translatesAutoresizingMaskIntoConstraints = false

        contentView!.addSubview(addButton)
        contentView!.addSubview(removeButton)

        NSLayoutConstraint.activate([
            addButton.leadingAnchor.constraint(equalTo: contentView!.leadingAnchor, constant: 0),
            addButton.topAnchor.constraint(equalTo: contentView!.topAnchor, constant: 0),
            addButton.bottomAnchor.constraint(equalTo: contentView!.bottomAnchor, constant: 0),
            addButton.widthAnchor.constraint(equalToConstant: 30)
        ])

        NSLayoutConstraint.activate([
            removeButton.leadingAnchor.constraint(equalTo: addButton.trailingAnchor, constant: -1),
            removeButton.topAnchor.constraint(equalTo: contentView!.topAnchor, constant: 0),
            removeButton.bottomAnchor.constraint(equalTo: contentView!.bottomAnchor, constant: 0),
            removeButton.widthAnchor.constraint(equalToConstant: 30)
        ])
    }

    fileprivate func setupStyle() {
        self.boxType = .custom
        self.alphaValue = 1
        self.borderColor = NSColor.gridColor
        self.borderType = .lineBorder
        self.borderWidth = 1
    }

}
About Pedro Piñera

I created XcodeProj and Tuist, and co-founded Tuist Cloud. My work is trusted by companies like Adidas, American Express, and Etsy. I enjoy building delightful tools for developers and open-source communities.