NSTouchBar 开发教程

完整系统的介绍了 Touch Bar 的应用架构,相关技术原理,接口协议,各种不同 Touch Bar 组件。开发的依赖环境,通过 xib 和代码实现 Touch Bar 的 Demo 代码实现的关键流程。

Apple 最新的 MacBook 对实体键盘做了革命性的变革,将键盘顶部实体功能按键改为了可触摸的虚拟动态键盘,称之为 Touch Bar。Touch Bar 可以由应用编程控制,每个应用可以将一些快捷功能入口放置到 Touch Bar 区域。可以说 Touch Bar 为桌面应用开发提供了更丰富有趣的交互体验形式,必将成为一些主流应用的新型有趣的交互方式。

touchBarView

系统区:根据不同情况显示为 Esc键,或者Cancel,Done 按钮。
应用区:应用程序可控制显示的区域。
控制区:系统级的控制键,像 Siri,音量键等。

NSTouchBar 从实现架构上来看类似 NSToolbar控件,NSTouchBar 代表的是个容器类,代表整体控件,包括多个单元,每个单元称为 item 用 NSTouchBarItem 类来表示。系统提供了多个 NSTouchBarItem 的子类来实现了各种特定的 item。
touchBarClass

每个应用有不同的 NSTouchBar ,因此 NSTouchBar 有唯一标识 customizationIdentifier,系统用来全局唯一的管理各个 TouchBar。

NSTouchBar 几个重要属性:

  1. customizationIdentifie 唯一标识
  2. customizationAllowedItemIdentifiers: 用户可以自定义选择的 item 的 标识符列表
  3. customizationRequiredItemIdentifiers:必须的 item 的 标识符列表,用户自定义选择时不能删除这些 item。 4.defaultItemIdentifiers:缺省显示的 item 列表。 5.principalItemIdentifier:关键的一个 item 标示。关键 item 会居中显示,或通过不同颜色区分表明它的重要性。

NSTouchBarItem 几个重要属性:

  1. identifier 唯一的标识符
  2. visibilityPriority: 显示的优先级,当应用区空白不够显示时,优先级低的 item 会被隐藏掉。
  3. view:item 具体的控件视图
  4. viewController:当 view 为空时,可以获取 viewController 的 view 做为 item 的 视图。
  5. isVisible: 是否可见。

响应链和嵌套组合

  1. 响应链

Touch Bar 是通过系统运行时,动态的遍历响应链获取。Touch Bar 显示的界面可以是当前响应链对象中定义的 item ,也可以是当前的与低一级响应链对象中定义的 item 的组合。甚至是当前响应链对象,低一级响应链对象,系统根据上下文对象预定义的 item 对象的组合,这种组合称之为嵌套组合。

下面图示了系统运行时遍历发现 Touch Bar 的响应链的优先级顺序为依次从下往上查找。

TouchBarResponderChain

  1. 嵌套组合的处理机制

应用定义的 TouchBar 可以做为容器,来嵌套显示低级响应链对象定义的 Touch Bar Item。

如果应用的 defaultItemIdentifiers 定义了 otherItemsProxy 这个 item,它实际上是一个低级响应链对象的展位符,表示系统在合适的情况下可以在此位置插入动态的上下文相关的 items。比如下面的 2种场景:

1)对于文本框获得输入焦点时,系统的 Candidate Lists Item 会自动出现。
2)对于 TextView 多行文本框获得输入焦点时,系统的 富文本相关的格式化按钮 item 自动出现。

如果应用没有定义 otherItemsProxy item,则应用定义的 Touch Bar 会被隐藏,而直接显示低等级的上下文 item。

下面是应用定义的 TouchBar item = A,B,C 和 item = A,B,C,otherItemsProxy 两种不同情况下运行时动态显示的结果。在 item = A,B,C 的情况下,可以看出当应用中没有 TextView获得焦点时能正常显示 ABC 三个按钮,如果 TextView获得焦点则富文本相关的 item 直接覆盖而 ABC的显示区域。而当 item = A,B,C,otherItemsProxy 则是 ABC 按钮跟富文本 item 按钮融合显示在了一起。

otherItemsProxyExample

在一个应有内部多个响应链对象之间也可以通过定义 otherItemsProxy 实现,高低响应链对象定义的 Touch Bar Item 融合显示。假如在 WindowController 和 ViewController 中同时定义了 Touch Bar Item,则在 WindowController 的 Touch Bar Item 增加一个 otherItemsProxy item,则优先显示 WindowController 的 Touch Bar Item 的情况下,ViewController 获得焦点有机会显示自己定义的 Touch Bar Item时,融合显示在 WindowController 的 Touch Bar 的 otherItemsProxy 位置。

NSTouchBar 接口和协议

NSResponder 对象增加了 touchBar 属性变量,和一个 makeTouchBar 方法。NSView,NSViewController,NSWindowController 都继承了 NSResponder,因此通过情况下只需要在 Controller 或 View 中覆盖实现 makeTouchBar 来定义 Touch Bar对象即可。

extension NSResponder : NSTouchBarProvider {
    @available(OSX 10.12.1, *)
    open var touchBar: NSTouchBar?
    @available(OSX 10.12.1, *)
    open func makeTouchBar() -> NSTouchBar?
}

NSTouchBarDelegate 代理协议定义根据 item 的 identifier 来构造 NSTouchBarItem 的接口方法。因此应用通过 Touch Bar 定义的 Item 来灵活定义每个具体的 NSTouchBarItem 显示的 View 视图控件。

public protocol NSTouchBarDelegate : NSObjectProtocol {  
    @available(OSX 10.12.1, *)
    optional public func touchBar(_ touchBar: NSTouchBar, makeItemForIdentifier identifier: NSTouchBarItemIdentifier) -> NSTouchBarItem?
}

NSTouchBar 控件分类

NSTouchBar 面板上可以放置的控件包括标准控件、TouchBar 专有的控件、自定义的 View 三大类。

标准控件:包括 NSButton,Label,NSSlider,NSSegmentedControl 等 AppKit 标准的控件。这些控件并不是控件工具箱中的标准控件,而是在颜色,外观,样式方面做了定制。

TouchBarBasicControlls

TouchBar 专有的控件

1.分组Group:;类似 NSBox,是一种视图容器,可以在内部放置其他 TouchBar 控件。用于控件分组便于统一控制一组控件,比如组内控件整体居中。

TouchBarGroup

2.Popovers

有 2个状态,初始状态是一个按钮,点击后展开一个面板,占据 Touch Bar 部分或整个区域,点击最左边的 Close 按钮,恢复为初始状态。

Popovers

3.Pickers

Pickers 是系统实现的特定功能的 Popovers 组件。

1)Character Pickers

表情输入选择的 Picker。

CharacterPickers

2)Color Pickers

颜色输入选择的 Picker。

ColorPickers

3)Sharing Service Pickers

社交分享选择的 Picker。

SharingServicePickers

4.Candidate Lists

文本输入框增强的联想输入的列表。文本框获取焦点的情况下,根据输入关键字 Touch Bar 应用区域可出现一组可选的字词列表。

CandidateLists

5.Scrubbers

可以左右滑动选择的一组按钮,图片等列表,外观上跟 ScrollView ,CollectionView 有些类似。

Scrubbers

6.对 TextView 增强

TextView 获得输入焦点时,Touch Bar 上出现的一组增强文本格式控制等功能按钮。

TouchBarTextView

自定义的 View

除了上述标准控件和专有的控件外,可以基于 NSView 来自定义控件。

TouchBarCustomView

开发环境准备

  1. Xcode 8.1
  2. macOS 10.12.1 ,编译版本必须大于等于 12B2657 。如果从 Mac Appstore 下载的最新的 macOS Sierra版本 10.12.1,是不支持 TouchBar 模拟器的,需要从 更新版本 地址下载更新安装包。 检查自己的 macOS 版本号,命令行敲入 sw_vers 查看版本号, 如下图。 NSPopoverView

环境满足的情况下,启动 Xcode,从 Window 菜单下看到 Show Touch Bar,点击可呼出 Touch Bar 模拟器,一个非常精致的黑色条面板。

ShowTouchBar

对于硬件不支持 Touch Bar 的 MacBook ,Touch Bar 模拟器是非常有用的,可以在没有 Touch Bar 的机器上测试应用的 Touch Bar 外观和相应事件。

使用 xib 创建 TouchBar

下图展示了控件工具箱中与 TouchBar 相关的一些控件对象,这些控件可以拖放到 View Controller 或 Window Controller 上来快速定义 Touch Bar Item。

TouchBarCntrolls

创建一个使用 Storyboard 的工程 TouchBarSBDemo,新建一个 NSWindowController 的子类 WindowController 。配置 Main.storyboard 中 WindowController 的 Class 为新建的 WindowController 类。

点击选中 storyboard 中的 WindowController ,从控件工具箱拖放一个 NSTouch Bar 控件到 WindowController 。同时依次拖放 Label、Button、SegmentControl、Slider、Character Picker、Color Pickers、Sharing Service Pickers 控件到 Touch Bar 控件上面,Label 修改 title 为 Demo,SegmentControl 两个选项分别为 iOS,macOS,完成布局如下图。

TouchBarSBDemo

对 Button,Segment,Slider 3个控件直接从属性面板创建 action 事件方法如下:

@IBAction func buttonAction(_ sender: Any) {
    print("clicked Button")
}

@IBAction func segmentAction(_ sender: Any) {
    let segment = sender as! NSSegmentedControl
    print("clicked segment index \(segment.selectedSegment)")
}

@IBAction func sliderAction(_ sender: Any) {
    let sliderItem = sender as! NSSliderTouchBarItem
    let slider = sliderItem.slider
    print("slider value \(slider.floatValue)")
}

Character Picker 选择的表情字符会自动输入到当前页面上获得焦点的输入框,因此没有提供 action 事件方法需要编程处理。

Color Pickers、Sharing Service Pickers 两个控件通过 xib 设置的事件响应方法都不会执行,因此需要按下面小节说明的使用代码创建的 Color Pickers、Sharing Service Pickers 完全编程去设置。

代码创建一个 TouchBar 的例子

创建一个使用 Storyboard 的工程 TouchBarNoSBDemo(NoSB意思是 TouchBar的创建使用代码完成),新建一个 NSWindowController 的子类 WindowController 。配置 Main.storyboard 中 WindowController 的 Class 为新建的 WindowController 类。

  1. 在 WindowController 中实现 makeTouchBar 方法创建 NSTouchBar。
fileprivate extension NSTouchBarCustomizationIdentifier {
    static let touchBar = NSTouchBarCustomizationIdentifier("io.Macdev.Demo")
}

fileprivate extension NSTouchBarItemIdentifier {
    static let label   = NSTouchBarItemIdentifier("io.Macdev.TouchBarItem.label")
    static let button  = NSTouchBarItemIdentifier("io.Macdev.TouchBarItem.button")
    static let segment = NSTouchBarItemIdentifier("io.Macdev.TouchBarItem.segment")
    static let slider  = NSTouchBarItemIdentifier("io.Macdev.TouchBarItem.slider")
    static let colorPicker  = NSTouchBarItemIdentifier("io.Macdev.TouchBarItem.colorPicker")
    static let sharePicker  = NSTouchBarItemIdentifier("io.Macdev.TouchBarItem.sharePicker")
}

override func makeTouchBar() -> NSTouchBar? {
    let touchBar = NSTouchBar()
    touchBar.delegate = self
    touchBar.customizationIdentifier = .touchBar
    touchBar.defaultItemIdentifiers = [.label,.button,.segment,.slider,.colorPicker,.characterPicker,.sharePicker]
    touchBar.customizationAllowedItemIdentifiers = [.label,.button,.segment]
    return touchBar
}
  1. 实现 NSTouchBarDelegate 代理协议

这里面是上面 defaultItemIdentifiers 中预定义的 item 的全部代码动态创建实现。

extension WindowController: NSTouchBarDelegate {
    func touchBar(_ touchBar: NSTouchBar, makeItemForIdentifier identifier: NSTouchBarItemIdentifier) -> NSTouchBarItem? {
        
        let custom = NSCustomTouchBarItem(identifier: identifier)
        
        switch identifier {
        case NSTouchBarItemIdentifier.label:
            custom.customizationLabel = "Demo"
            let label = NSTextField.init(labelWithString: "Demo")
            custom.view = label
            return custom
            
        case NSTouchBarItemIdentifier.button:
            custom.customizationLabel = "Button"
            let button = NSButton(title: "Button", target: self, action: #selector(buttonAction))
            custom.view = button
            return custom
            
            
        case NSTouchBarItemIdentifier.segment:
           let segment = NSSegmentedControl(labels: ["iOS","macOS"], trackingMode: .momentary, target: self, action: #selector(segmentAction))
            custom.view = segment
            return custom
        
            
        case NSTouchBarItemIdentifier.slider:
            let sliderItem = NSSliderTouchBarItem(identifier: identifier)
            sliderItem.label = "Size"
            sliderItem.customizationLabel = "Font Size"
            let slider = sliderItem.slider
            slider.minValue = 1.0
            slider.maxValue = 10.0
            slider.target = self
            slider.action = #selector(sliderAction)
            slider.integerValue = 2
            return sliderItem
            
        case NSTouchBarItemIdentifier.colorPicker:
            let colorPicker = NSColorPickerTouchBarItem.colorPicker(withIdentifier: identifier)
            colorPicker.customizationLabel = "Color Picker"
            colorPicker.target = self
            colorPicker.action = #selector(colorDidPick(_:))
            return colorPicker
            
        case NSTouchBarItemIdentifier.sharePicker:
            let services = NSSharingServicePickerTouchBarItem(identifier: identifier)
            services.delegate = self
            return services
            
        default:
            return custom
        }
    }
}

3.代理接口中创建的控件对象相关的 Action 方法

@IBAction func buttonAction(_ sender: Any) {
    print("clicked Button")
}

@IBAction func segmentAction(_ sender: Any) {
    let segment = sender as! NSSegmentedControl
    print("clicked segment index \(segment.selectedSegment)")
}

@IBAction func sliderAction(_ sender: NSSlider) {
    print("slider value \(sender.floatValue)")
}

@IBAction func colorAction(_ sender: Any) {
    let item = sender as! NSColorPickerTouchBarItem
    print("color value \(item.color)")
}

func colorDidPick(_ colorPicker: NSColorPickerTouchBarItem) {
    print("Picked color: \(colorPicker.color)")
}

TouchBar 高级组件使用

  1. Group

Group 是逻辑分组,便于对多个 item 控件分组整体控制。

TouchBarGroupComponent

分组控制的示例代码如下(省略了 identifier 变量的定义):

makeTouchBar 中定义了 NSTouchBar,它包括 2个分组。

override func makeTouchBar() -> NSTouchBar? {
let touchBar = NSTouchBar()
touchBar.delegate = self
touchBar.customizationIdentifier = .touchBar
touchBar.defaultItemIdentifiers = [.group1,.group2]
//touchBar.principalItemIdentifier = .group1
return touchBar
}

NSTouchBarDelegate 代理协议中 ,对于组 item,根据 Identifier 参数, 创建 NSGroupTouchBarItem ,它有一个 NSTouchBar 类型的属性 groupTouchBar 。组 groupTouchBar 的定义跟之前的是一样。

extension WindowController: NSTouchBarDelegate {
    func touchBar(_ touchBar: NSTouchBar, makeItemForIdentifier identifier: NSTouchBarItemIdentifier) -> NSTouchBarItem? {
        
        let custom = NSCustomTouchBarItem(identifier: identifier)
        
        switch identifier {
            
        case NSTouchBarItemIdentifier.group1:
           
            let group1 = NSGroupTouchBarItem(identifier: identifier)
            let groupTouchBar = NSTouchBar()
            groupTouchBar.delegate = self
            groupTouchBar.defaultItemIdentifiers = [.label,.button];
            group1.groupTouchBar = groupTouchBar
            return group1
            
        case NSTouchBarItemIdentifier.group2:
            
            let group2 = NSGroupTouchBarItem(identifier: identifier)
            let groupTouchBar = NSTouchBar()
            groupTouchBar.delegate = self
            groupTouchBar.defaultItemIdentifiers = [.segment,.slider];
            group2.groupTouchBar = groupTouchBar
            
            return group2
            
        case NSTouchBarItemIdentifier.label:
            custom.customizationLabel = "Demo"
            let label = NSTextField.init(labelWithString: "Demo")
            custom.view = label
            return custom
            
        default:
            return custom
        }
    }
}
  1. Popover

Popover 除了显示方式,定义实现跟 Group 基本一致,也是需要定义一个 2级的 TouchBar。

let popoverItem = NSPopoverTouchBarItem(identifier: identifier)
popoverItem.customizationLabel = "Font Size"
popoverItem.collapsedRepresentationLabel = "Font Size"

let secondaryTouchBar = NSTouchBar()
secondaryTouchBar.delegate = self
secondaryTouchBar.defaultItemIdentifiers = [.slider];

popoverItem.pressAndHoldTouchBar = secondaryTouchBar
popoverItem.popoverTouchBar      = secondaryTouchBar
popoverItem.visibilityPriority = .high

return popoverItem
  1. Scrubber

类似滚动的 ScrollView,可以左右滑动来显示更多的信息。Scrubber 提供了三种视图类 NSScrubberTextItemView、NSScrubberImageItemView、NSScrubberItemView,分别用来显示文本 item,图像 item 和 完全自定义的 View。

由于 Scrubber 中存在多个数据,因此采用了 Cocoa 常用的数据源和代理的类分层的架构,分别通过 NSScrubberDataSource 和 NSScrubberDelegate 提供了标准的接口方法。

NSScrubberDataSource 接口定义了数据集的元素个数,以及每个 item 对应的 View。

public protocol NSScrubberDataSource : NSObjectProtocol {
    
    @available(OSX 10.12.1, *)
    public func numberOfItems(for scrubber: NSScrubber) -> Int

    @available(OSX 10.12.1, *)
    public func scrubber(_ scrubber: NSScrubber, viewForItemAt index: Int) -> NSScrubberItemView
}

NSScrubberDelegate 中定义了选中的元素的索引,高亮的元素,可视化区域变化的接口。

public protocol NSScrubberDelegate : NSObjectProtocol {

    @available(OSX 10.12.1, *)
    optional public func scrubber(_ scrubber: NSScrubber, didSelectItemAt selectedIndex: Int)

    @available(OSX 10.12.1, *)
    optional public func scrubber(_ scrubber: NSScrubber, didHighlightItemAt highlightedIndex: Int)

    @available(OSX 10.12.1, *)
    optional public func scrubber(_ scrubber: NSScrubber, didChangeVisibleRange visibleRange: NSRange)

}

下面是一个文本单元的例子示例代码。

1) 定义 TouchBar

override func makeTouchBar() -> NSTouchBar? {
    let touchBar = NSTouchBar()
    touchBar.delegate = self
    touchBar.customizationIdentifier = .touchBar
    touchBar.defaultItemIdentifiers = [.textScrubber]
    return touchBar
}

2) 实现 NSTouchBarDelegate 代理协议

extension WindowController: NSTouchBarDelegate {
    func touchBar(_ touchBar: NSTouchBar, makeItemForIdentifier identifier: NSTouchBarItemIdentifier) -> NSTouchBarItem? {
        
            case NSTouchBarItemIdentifier.textScrubber:
            
            let custom = NSCustomTouchBarItem(identifier: identifier)
            
            let scrubber = NSScrubber()
            //流式布局
            scrubber.scrubberLayout = NSScrubberFlowLayout()
            //注册缓存可重用的类,如果显示图片则注册为图片类型的 NSScrubberImageItemView 
            scrubber.register(NSScrubberTextItemView.self, forItemIdentifier: itemTextViewIdentifier)
            scrubber.mode = .fixed
            scrubber.selectionBackgroundStyle = .roundedBackground
            //配置代理和数据源
            scrubber.delegate   = self
            scrubber.dataSource = self

            //绑定 scrubber 对象到 item 的视图
            custom.view = scrubber
            
            return custom
            
        default:
            return custom
        }
    }
}
    

3) Scrubber 的数据源 代理协议实现

extension WindowController: NSScrubberDelegate {
    func scrubber(_ scrubber: NSScrubber, didSelectItemAt index: Int) {
        print("\(#function) at index \(index)")
    }
}

extension WindowController: NSScrubberDataSource {
    func numberOfItems(for scrubber: NSScrubber) -> Int {
        return 20
    }
    
    func scrubber(_ scrubber: NSScrubber, viewForItemAt index: Int) -> NSScrubberItemView {
        let itemView = scrubber.makeItem(withIdentifier:itemTextViewIdentifier,
                                         owner: nil) as! NSScrubberTextItemView
        itemView.textField.stringValue = String(index)
        itemView.textField.backgroundColor = NSColor.blue
        
        return itemView
    }
}

extension WindowController: NSScrubberFlowLayoutDelegate {
    
    func scrubber(_ scrubber: NSScrubber, layout: NSScrubberFlowLayout, sizeForItemAt itemIndex: Int) -> NSSize {
        return NSSize(width: 60, height: 30)
    }
}

总结一下,对于 NSScrubber 类型的 item,创建 NSScrubber 对象时首先保证 register 注册正确的单元类型。另外对于数据源协议中正确的实现 scrubber:viewForItemAt: 方法返回合适的单元 item 对象。

完全定制的 TouchBar

自定义一个 NSView 的子类 CustomView,CustomView 界面完全可以自己通过 draw 方法绘制。
界面事件可以通过 touchesBegan、touchesMoved、touchesEnded、touchesCancelled 获得当前移动的 X 坐标。具体实现可以参加 Apple 的 NSTouchBarCatalog 中的例子。

let customItem = NSCustomTouchBarItem(identifier: identifier)

let customView = CustomView()
customView.wantsLayer = true
customView.layer?.backgroundColor = NSColor.blue.cgColor
customView.allowedTouchTypes = .direct

customItem.view = customView

动态改变 TouchBar

通过动态绑定来实时刷新显示不同的 Touch Bar。对于 View Controller 可以绑定自己的 touchBar 到 Window 的 TouchBar 来做到优先显示。

 deinit {
        self.view.window?.unbind(#keyPath(touchBar))
    }
    
    override func viewDidAppear() {
        super.viewDidAppear()
        if #available(OSX 10.12.1, *) {
           self.view.window?.unbind(#keyPath(touchBar)) // unbind first
           self.view.window?.bind(#keyPath(touchBar), to: self, withKeyPath: #keyPath(touchBar), options: nil)
        }
    }

完整的 《Swift macOS 应用开发教程》电子版图书请通过下面的联系方式获取相关购买和支持信息。

联系方式

macOS App 开发专业网站: http://www.macdev.io

微博帐号:剑指人心

微信公众号:MacAppDev

扫一扫关注微信公众号

MacAppDevWeixin