macosappkitprogrammatically-creatednstoolbarnstoolbaritem

NSToolbar with NSView subclass custom view does not render in customise toolbar panel


I have created some NSToolbarItems programmatically with custom views in an AppKit app. The toolbar items are appearing (and functioning) just fine in the toolbar. The issue arises when the toolbar is being customised.

The NSToolbarItem with NSView-subclass custom view does render in toolbar and the "... or drag the default set into the toolbar" area of the customisation panel (the bottom bit), but not in the "Drag your favourite items into the toolbar ..." area of the customisation panel (the top bit). The same code generates all 3 NSToolbarItem instances.

Toolbar items using NSButton, NSTextField and other controls as custom views do render in all case, but not those based on NSView. What do I need to do to make NSView-based custom toolbar items visible in both areas of the customise toolbar pane?

enter image description here

Here's a cut-down version of the NSWindowController which creates the toolbar (and acts as the toolbar's delegate)

import AppKit

public class DocumentWindowController : NSWindowController, NSToolbarDelegate {
    
    private struct ToolbarItemIdentifiers {
        fileprivate static let addPhotos = NSToolbarItem.Identifier("add-photos")
        fileprivate static let thumbnailSize = NSToolbarItem.Identifier("thumbnail-size")
    }
    
    public override init (
        window: NSWindow?
    ) {
        super.init(window: window)

        let toolbar = NSToolbar(identifier: "toolbar-identifier.document")
        toolbar.delegate = self
        toolbar.allowsUserCustomization = true
        toolbar.autosavesConfiguration = true
        toolbar.displayMode = .default
        window?.toolbarStyle = .unified
        window?.titleVisibility = .visible
        window?.toolbar = toolbar
    }
    
    // MARK: - NSToolbarDelegate
    
    public func toolbar (
        _ toolbar: NSToolbar,
        itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier,
        willBeInsertedIntoToolbar flag: Bool
    ) -> NSToolbarItem? {
        switch itemIdentifier {
            case ToolbarItemIdentifiers.addPhotos:
                let toolbarItem = NSToolbarItem(itemIdentifier: itemIdentifier)
                toolbarItem.label = "Add Photos"
                toolbarItem.image = NSImage(systemSymbolName: "plus", accessibilityDescription: "Add Photos Icon")
                toolbarItem.target = self
                toolbarItem.action = #selector(self.addPhotosButtonTapped)
                return toolbarItem
            case ToolbarItemIdentifiers.thumbnailSize:
                let toolbarItem = NSToolbarItem(itemIdentifier: itemIdentifier)
                toolbarItem.label = "Thumbnail Size"
                toolbarItem.view = ThumbnailSizeView() // an unremarkable NSView subclass
                return toolbarItem
            default:
                return nil
        }
    }
    
    public func toolbarAllowedItemIdentifiers (_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
        return [
            ToolbarItemIdentifiers.addPhotos,
            ToolbarItemIdentifiers.thumbnailSize
        ]
    }
    
    public func toolbarDefaultItemIdentifiers (_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
        return [
            ToolbarItemIdentifiers.addPhotos,
            ToolbarItemIdentifiers.thumbnailSize
        ]
    }
}

class ThumbnailSizeView : NSView {
    
    private let slider = NSSlider()
    
    init(min minValue: Int64, max maxValue: Int64, initial initialValue: Int64) {
        super.init(frame: .zero)
        
        let minusIcon = NSImageView()
        minusIcon.image = NSImage(
            systemSymbolName: "minus",
            accessibilityDescription: "Smaller"
        )?.withSymbolConfiguration(.init(scale: .small))
        minusIcon.translatesAutoresizingMaskIntoConstraints = false
        minusIcon.setContentCompressionResistancePriority(.required, for: .horizontal)
        minusIcon.setContentHuggingPriority(.required, for: .horizontal)
        addSubview(minusIcon)
        NSLayoutConstraint.activate([
            minusIcon.leadingAnchor.constraint(equalTo: leadingAnchor),
            minusIcon.centerYAnchor.constraint(equalTo: centerYAnchor)
        ])
        
        slider.minValue = Double(minValue)
        slider.maxValue = Double(maxValue)
        slider.intValue = Int32(initialValue)
        slider.sliderType = .linear
        slider.controlSize = .mini
        slider.translatesAutoresizingMaskIntoConstraints = false
        addSubview(slider)
        NSLayoutConstraint.activate([
            slider.leadingAnchor.constraint(equalTo: minusIcon.trailingAnchor, constant: 4),
            slider.centerYAnchor.constraint(equalTo: centerYAnchor),
            slider.widthAnchor.constraint(equalToConstant: 66)
        ])
        
        let plusIcon = NSImageView()
        plusIcon.image = NSImage(
            systemSymbolName: "plus",
            accessibilityDescription: "Larger"
        )?.withSymbolConfiguration(.init(scale: .small))
        plusIcon.translatesAutoresizingMaskIntoConstraints = false
        plusIcon.setContentCompressionResistancePriority(.required, for: .horizontal)
        plusIcon.setContentHuggingPriority(.required, for: .horizontal)
        addSubview(plusIcon)
        NSLayoutConstraint.activate([
            plusIcon.leadingAnchor.constraint(equalTo: slider.trailingAnchor, constant: 4),
            plusIcon.centerYAnchor.constraint(equalTo: centerYAnchor),
            plusIcon.trailingAnchor.constraint(equalTo: trailingAnchor)
        ])
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
}

For context, I'm using Xcode 14.3.1 on macOS Ventura 13.5.1


Solution

  • The ThumbnailSizeView is missing a height constraint and its height is zero. Add a height constraint to fix the issue.

    NSLayoutConstraint.activate([
        slider.leadingAnchor.constraint(equalTo: minusIcon.trailingAnchor, constant: 4),
        slider.heightAnchor.constraint(equalTo: heightAnchor),
        slider.widthAnchor.constraint(equalToConstant: 66)
    ])