Deploy NSTouchBar on Swift

Recently, Apple introduced the world a new line of MacBook Pro. And one of the features of the latest version was that the top row of system buttons in it was removed - or rather, replaced with a multi-touch screen. Developers should be interested in this innovation first of all, because the panel highlights an area that can be used in your own applications. Apple has even provided an API for its use. In this article we will tell and show how you can apply the features of NSTouchBar. And then they can be used to work on something significant, which was done in the update MaCleaner from our colleagues.



First, let's look at what NSTouchBar is all about.



Application region- this, in fact, is the very part that is assigned to the needs of the application. It is here that everything that is necessary for its correct operation will be displayed.

Control Strip - system panel. The developer’s entrance is closed here - we can’t either modify our objects or somehow modify it. Here are the buttons that used to be in place of the NSTouchBar - changing brightness, volume, and others.

System Button - a system button

In order to implement NSTouchBar support in your projects, it is not necessary to have a brand new MacBookPro with a touchbar. Xcode has its simulator, which is called through Window> Show Touch Bar. However, you can use it only if your Xcode version is at least 8.1, and the system has 10.12.1, and with a build of at least 16B2657.

So, you worked hard and wrote a wonderful application - it hangs in the status bar, shows the system design of the desktop in a pop-up window and changes it to the outfit that the user selects. It all looks like this:



If you caught fire with this wonderful application - you can download the version without NSTouchBar here and work on it with us.

Now let's introduce NSTouchBar support in our project. To get started, connect the NSTouchBar and display “Hello World!” In it. Add the following extension to our MainPopoverController:

@available(OSX 10.12.1, *)
extension MainPopoverController : NSTouchBarDelegate {
    override open func makeTouchBar() -> NSTouchBar? {
        let touchBar = NSTouchBar()
        touchBar.delegate = self
        touchBar.customizationIdentifier =  NSTouchBarCustomizationIdentifier("My First TouchBar")
        touchBar.defaultItemIdentifiers = [NSTouchBarItemIdentifier("HelloWorld")]
        touchBar.customizationAllowedItemIdentifiers = [NSTouchBarItemIdentifier("HelloWorld")]
        return touchBar
    }
}

In the makeTouchBar () method, we create an NSTouchBar object and specify its delegate. Each NSTouchBar and each NSTouchBarItem must have unique identifiers. Therefore, we prescribe the identifier for our NSTouchBar, then specify the identifiers of the objects that we put in it, and, finally, set the display order of the objects. Now add a method that will populate our NSTouchBar with objects:

func touchBar(_ touchBar: NSTouchBar, makeItemForIdentifier identifier: NSTouchBarItemIdentifier) -> NSTouchBarItem? {
        switch identifier {
        case NSTouchBarItemIdentifier("HelloWorld"):
            let customViewItem = NSCustomTouchBarItem(identifier: identifier)
            customViewItem.view = NSTextField(labelWithString: "Hello World!")
            return customViewItem
        default:
            return nil
        }
}

In this method, you can fill in the NSTouchBar as you wish. For this example, we created an NSCustomTouchBarItem and put an NSTextField with the text “Hello World!” In it. Now it's time to launch the project and enjoy our exclusive NSTouchBar! We launch - and ...



Where is our “Hello World!”? But the thing is this. In order for us to generate an NSTouchBar in the MainPopoverController, at least one object in the popover must get focus. There are two objects in our popover - NSView itself and CollectionView. For NSView (and CollectionView), acceptsFirstResponder always returns false by default - this way, when we launch the application, focus is not placed on any object, which means that NSTouchBar is not generated either. We will get out of the situation as follows: create our own NSView and redefine acceptsFirstResponder so that it returns true.

class MainPopoverView: NSView {
    override var acceptsFirstResponder: Bool {
        get {
            return true
        }
    }
}

Let's try to run the application after using this class in our popover and see what happened:



Our NSTouchBar finally showed itself to the world and even greeted this world. Now we need to implement NSTouchBar applicable to the functionality of our application. We want to put all the pictures from our collection into NSTouchBar so that when you click on the picture, the application sets its background to the desktop. Since there is not enough space for the entire gallery in NSTouchBar, we will use the scrollable NSScrubber. First, let's change the method that populates our NSTouchBar with objects so that it uses NSScrubber.

func touchBar(_ touchBar: NSTouchBar, makeItemForIdentifier identifier: NSTouchBarItemIdentifier) -> NSTouchBarItem? {
        switch identifier {
        case NSTouchBarItemIdentifier("HelloWorld"):
            let scrubberItem = NSCustomTouchBarItem(identifier: identifier)
            let scrubber = NSScrubber()
            scrubber.scrubberLayout = NSScrubberFlowLayout()
            scrubber.register(NSScrubberImageItemView.self, forItemIdentifier: "ScrubberItemIdentifier")
            scrubber.mode = .free
            scrubber.selectionBackgroundStyle = .roundedBackground
            scrubber.delegate = self
            scrubber.dataSource = self
            scrubberItem.view = scrubber
            return scrubberItem
        default:
            return nil
        }
}

In order to populate NSScrubber with objects, you must subscribe to NSScrubberDataSource; to handle clicking on a specific object from it, to NSScrubberDelegate, and to specify the size of the object (and in NSTouchBar the maximum height of the object is 30, otherwise the object will be cropped) to NSScrubberFlowLayoutDelegate.

As a result, we add the following extension for our MainPopoverController:

@available(OSX 10.12.1, *)
extension MainPopoverController: NSScrubberDataSource, NSScrubberDelegate, NSScrubberFlowLayoutDelegate {
    func numberOfItems(for scrubber: NSScrubber) -> Int {
        return wardrobe.wallpapers.count
    }
    func scrubber(_ scrubber: NSScrubber, viewForItemAt index: Int) -> NSScrubberItemView {
        let itemView = scrubber.makeItem(withIdentifier: "ScrubberItemIdentifier", owner: nil) as! NSScrubberImageItemView
        itemView.image = wardrobe.wallpapers[index].picture
        return itemView
    }
    func scrubber(_ scrubber: NSScrubber, didSelectItemAt index: Int) {
        scrubber.selectedIndex = -1
        if let screen = NSScreen.main() {
            do {
                try NSWorkspace.shared().setDesktopImageURL( wardrobe.wallpapers[index].url, for: screen, options: [:])
            } catch {
                print(error)
            }
        }
    }
    func scrubber(_ scrubber: NSScrubber, layout: NSScrubberFlowLayout, sizeForItemAt itemIndex: Int) -> NSSize {
        return NSSize(width: 60, height: 30)
    }
}

The first method returns the number of objects in NSScrubber. The second - generates an object for a specific position in NSScrubber (in our case, these are pictures for the desktop). The third - handles clicking on an object in NSScrubber and puts the selected image on the desktop. Well, the fourth method returns the size of the object in NSScrubber.

Now, at startup, our NSTouchBar looks like this:



Now we have a working NSTouchBar in our application that can change the appearance of the desktop. Using the same algorithm, you can implement touchbar support in more complex projects, which we plan to do in the future.

Thank you for your attention and good luck with the new gimmick from Apple!

Also popular now: