1024jp 1024jp - 11 days ago 14
Swift Question

Best practical way to validate NSTouchBar items

On AppKit, menu items and toolbar items have

validateMenuItem(_:)
and
validateToolbarItem(_:)
respectively. However, by new touch bar items, there is no such convenience method to validate appropriate items at the right moment.

I'm now validating touch bar items every time when I change the related values and invoke a validation method in
didSet
(see the following sample code). But I feel it is not a good way because the related values must know there is a touch bar item depending on it.

var foo: Foo? {
didSet {
if #available(macOS 10.12.1, *), NSClassFromString("NSTouchBar") != nil {
self.validateTouchBarItem(identifier: .foo)
}
}
}


@available(macOS 10.12.1, *)
func validateTouchBarItem(identifier: NSTouchBarItemIdentifier) {

guard
let item = self.touchBar?.item(forIdentifier: identifier),
let button = item.view as? NSButton
else { return }

switch identifier {
case NSTouchBarItemIdentifier.foo:
button.isEnabled = (self.foo != nil)

default: break
}
}


Another way I'm using is the Cocoa-binding and KVO, however, it doesn't always work well.

So, I'm curious whether there is any recommended or a defacto-standard way to validate touch bar items, especially containing NSButton and NSSegmentedControl. I wanna change not only the availability of items but sometimes also images or colors of them depending on the situation. How do you guys validate touch bar items?

Answer

I improved my touch bar validation system by myself and made the following protocol and extensions.

@available(macOS 10.12.1, *)
protocol TouchBarItemValidations: class {

    func validateTouchBarItem(_ item: NSTouchBarItem) -> Bool
}



@available(macOS 10.12.1, *)
extension NSTouchBarProvider {

    func validateTouchBarItems() {

        guard NSClassFromString("NSTouchBar") != nil else { return }  // run-time check

        guard let touchBar = self.touchBar else { return }

        // validate currently visible touch bar items
        for identifier in touchBar.itemIdentifiers {
            guard let item = touchBar.item(forIdentifier: identifier) as? NSCustomTouchBarItem else { continue }

            item.validate()
        }
    }

}


@available(macOS 10.12.1, *)
extension NSCustomTouchBarItem: NSValidatedUserInterfaceItem {

    func validate() {

        // validate content control
        if let control = self.control,
            let action = control.action,
            let validator = NSApp.target(forAction: action, to: control.target, from: self)
        {
            if let validator = validator as? TouchBarItemValidations {
                control.isEnabled = validator.validateTouchBarItem(self)

            } else if let validator = validator as? NSUserInterfaceValidations {
                control.isEnabled = (validator as AnyObject).validateUserInterfaceItem(self)
            }
        }
    }



    // MARK: Validated User Interface Item Protocol

    public var action: Selector? {

        return self.control?.action
    }


    public var tag: Int {

        return self.control?.tag ?? 0
    }



    // MARK: Private Methods

    private var control: NSControl? {

        return self.view as? NSControl
    }

}

It requires subclassing NSWindow that uses touchBar (or NSApplication's updateWindows() if you add touchBar to the application level), but except for that, nothing complex to add. You can now use validateTouchBarItem(_:) or normal validateUserInterfaceItem(_:) to validate your own touch bar items!

// MARK: Touch Bar Validation
class Window: NSWindow {

    override func update() {

        super.update()

        if #available(macOS 10.12.1, *) {
            for responder in sequence(first: self.firstResponder, next: { $0.nextResponder }) {
                responder.validateTouchBarItems()
            }
        }
    }

}

I suppose this works well at least for my requirements.