Search code examples
swiftcocoaappkitmacos-sierranstouchbar

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?


Solution

  • 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
        }
    
    }
    
    
    @available(macOS 10.12.1, *)
    extension AppDelegate {
    
        func applicationDidUpdate(_ notification: Notification) {
    
            // validate touch bar items
            if #available(macOS 10.12.1, *) {
                if let window = NSApp.mainWindow {
                    for responder in sequence(first: window.firstResponder, next: { $0.nextResponder }) {
                        responder.validateTouchBarItems()
                    }
                }
            }
        }
    }
    

    You should modify AppDelegate's applicationDidUpdate(:_) if you already have one, but except for that, nothing complex to add. You can now use validateTouchBarItem(_:) or normal validateUserInterfaceItem(_:) to validate your own touch bar items!

    I suppose this works well at least for my requirements.