Touch Bar 快速开发入门

1,734 阅读5分钟

这里必须吐槽简书一下,写了那么长的文章保存后关闭一下竟然就没了??程序员改罚这月的工资了!

算了,没心情再长篇大论得写了,简单说说吧。

本文要实现的效果如下:



Touch Bar 中的内容从左到右依次是两个 Popover 按钮、一个嵌套面板、一个自定义颜色风格的按钮。

然后我们来看实现。

首先要知道,Touch Bar 的显示是取决于响应链的,First Responder 为响应链的最顶端,NSApplication 为响应链的最底端,系统会从最底端开始冒泡执行 NSRespondermakeTouchBar 方法,这个方法会返回一个 NSTouchBar 对象。NSTouchBar 并不是一个 view,而应该是一个 model,它通过 identifier 来指定其包含的内容,然后系统根据这些 identifer 来向 delegate 索要 NSTouchBarItem。identifier 的类型就是 NSString,自己取值就好了,通常是 "com.<项目名>.<类名>.xxxx" 的模式。

NSTouchBarItem 也不是一个 view,它有一个 view 属性,表示真正显示的 view。它有很多派生类,关于它们的用法大家看 API 文档就好了,介绍得很详细。

在开发中,我们需要实现 NSResponder 子类的 makeTouchBar 方法,通常情况我们实现 Window Controller 的就好了,因为每个窗口的 Touch Bar 可能不尽相同,而 View Controller 和 View 也可以实现自己的 Touch Bar。
我们来看看例子中 makeTouchBar 方法:

- (NSTouchBar *)makeTouchBar {
    NSTouchBar *touchBar = [[NSTouchBar alloc] init];
    touchBar.delegate = self;
    touchBar.customizationIdentifier = ViewControllerCustomizationIdentifier;
    touchBar.defaultItemIdentifiers = @[FontSizeItemIdentifier, FontFamilyItemIdentifier, NSTouchBarItemIdentifierOtherItemsProxy, ResetStyleIdentifier];

    return touchBar;
}

可以看到,这个 Touch Bar 正好有我们界面中所示的那四个元素,其中的 NSTouchBarItemIdentifierOtherItemsProxy 是一个代理项,它是用来显示比自己更接近响应链顶端的 Touch Bar 的。举个例子,我们的窗口里有一个 NSTextView,它有自己的 Touch Bar,而它在获取焦点的时候就是顶端的 NSResponder,所以系统会选择现实它的 Touch Bar。但是如果在冒泡过程中,系统发现它的上一级 NSResponder 的 Touch Bar 中有代理项,那么系统就会把 First Responder 的 Touch Bar 嵌入到这个 Touch Bar 的代理项中。但是如果某一层 Touch Bar 没有包含代理项,那么系统只能单独显示 First Responder 的 Touch Bar 了。

这里提一下如何刷新 Touch Bar,有时候你可能想重置 Touch Bar 的内容,最简单的方法就是 self.touchBar = nil,这时系统会重新调用 makeTouchBar 来创建新的 Touch Bar,效果就相当于 Table View 的 reloadData

然后我们来看一下代理方法:

- (NSTouchBarItem *)touchBar:(NSTouchBar *)touchBar makeItemForIdentifier:(NSTouchBarItemIdentifier)identifier {
    if ([identifier isEqualToString:FontSizeItemIdentifier]) {
        NSPopoverTouchBarItem *item = [[NSPopoverTouchBarItem alloc] initWithIdentifier:identifier];
        item.collapsedRepresentationLabel = @"Font Size";
        item.showsCloseButton = YES;

        NSTouchBar *secondaryTouchBar = [[NSTouchBar alloc] init];
        secondaryTouchBar.delegate = self;
        secondaryTouchBar.defaultItemIdentifiers = @[FontSizeSliderItemIdentifier];

        item.pressAndHoldTouchBar = secondaryTouchBar;
        item.popoverTouchBar = secondaryTouchBar;

        return item;
    }

    if ([identifier isEqualToString:FontSizeSliderItemIdentifier]) {
        NSSliderTouchBarItem *item = [[NSSliderTouchBarItem alloc] initWithIdentifier:identifier];
        item.label = @"Font Size";
        item.slider.minValue = 10;
        item.slider.maxValue = 72;
        item.slider.floatValue = [NSFontManager sharedFontManager].selectedFont.pointSize;
        item.slider.target = self;
        item.slider.action = @selector(sliderDidChange:);
        [item.slider addConstraint:[NSLayoutConstraint constraintWithItem:item.slider attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:250]];

        return item;
    }

    if ([identifier isEqualToString:FontFamilyItemIdentifier]) {
        NSPopoverTouchBarItem *item = [[NSPopoverTouchBarItem alloc] initWithIdentifier:identifier];
        item.collapsedRepresentationLabel = @"Font Family";

        NSTouchBar *secondaryTouchBar = [[NSTouchBar alloc] init];
        secondaryTouchBar.delegate = self;
        secondaryTouchBar.defaultItemIdentifiers = @[FontFamilyScrubberItemIdentifier];

        item.popoverTouchBar = secondaryTouchBar;

        return item;
    }

    if ([identifier isEqualToString:FontFamilyScrubberItemIdentifier]) {
        FontFamilyTouchBarItem *item = [[FontFamilyTouchBarItem alloc] initWithIdentifier:identifier];

        return item;
    }

    if ([identifier isEqualToString:ResetStyleIdentifier]) {
        NSCustomTouchBarItem *item = [[NSCustomTouchBarItem alloc] initWithIdentifier:identifier];

        NSMutableAttributedString *titleString = [[[NSAttributedString alloc] initWithString:@"Reset Style" attributes:@{NSForegroundColorAttributeName: [NSColor blackColor], NSFontAttributeName: [NSFont systemFontOfSize:0]}] mutableCopy];
        [titleString setAlignment:NSTextAlignmentCenter range:NSMakeRange(0, titleString.length)];

        NSButton *button = [[NSButton alloc] init];
        button.bezelStyle = NSBezelStyleRounded;
        button.bezelColor = [NSColor colorWithCalibratedRed:1.00 green:0.81 blue:0.21 alpha:1.00];
        button.attributedTitle = titleString;

        item.view = button;

        return item;
    }

    return nil;
}

这里就是根据 identifier 去创建 NSTouchBarItem 了。
NSPopoverTouchBarItem 表示一个可以展开的项目,collapsedRepresentationLabel 属性可以制定其未展开时按钮的标题,当然你也可以制定一个自定义 view,如果是自定义 view,你需要让 view 在适当的时机(比如按下时)之行 NSPopoverTouchBarItemshowPopover 方法来展开 popover。Popover 的内容是另外一个 NSTouchBar 对象,它也有自己的 identifier 和 delegate,创建好子 Touch Bar 后,将其赋给 popoverTouchBar 属性就可以了,如果你想支持按下后滑动选择的效果,可以再把它赋给 pressAndHoldTouchBar 属性,但这时的子 Touch Bar 应当只包含一个 Slider,不然不会有正常的行为。

NSSliderTouchBarItem 表示一个滑动条,里面有一个 slider 属性,拿出来就可以当普通的 NSSlider 用,给它设置 target-action 等。默认情况下,slider 会填满整个 Touch Bar,如果你不想这样,可以像上面代码一样,给 slider 添加一个 constraint。

普通按钮项需要借助 NSCustomTouchBarItem 来实现,把它的 view 属性设置为一个 NSButton 就好了,但是要注意 button 的 bezelStyle 属性,一定是 NSBezelStyleRounded,按钮的背景颜色和文本颜色的修改方式大家看代码就能明白了。AttributedString 的字体属性用 [NSFont systemFontOfSize:0] 来表示一个默认值,由于自己创建的字符串不包含对其属性,默认是居左的,所以还要设置一下对其方式,让其居中。

下面来说说 NSScrubber 这个东西,它是一个用来从一组选项中选取一项的控件,支持滑动选择


通常使用它你需要子类化一个 NSCustomTouchBarItem, 在 Popover 的 Touch Bar 里把它添加进去。在子类初始化方法中配置 NSScrubber

- (instancetype)initWithIdentifier:(NSTouchBarItemIdentifier)identifier {
    self = [super initWithIdentifier:identifier];
    if (self) {
        [self setup];
    }
    return self;
}

- (void)setup {
    NSScrubber *scrubber = [[NSScrubber alloc] init];
    scrubber.scrubberLayout = [[NSScrubberFlowLayout alloc] init];
    scrubber.mode = NSScrubberModeFree;
    scrubber.selectionBackgroundStyle = [NSScrubberSelectionStyle outlineOverlayStyle];
    scrubber.delegate = self;
    scrubber.dataSource = self;
    [scrubber registerClass:[NSScrubberTextItemView class] forItemIdentifier:TextItemIdentifier];

    self.fontNames = @[@"Arial", @"Courier", @"Gill Sans", @"Helvetica", @"Impact", @"Menlo", @"Times New Roman", @"苹方", @"手札体", @"娃娃体", @"圆体"];

    self.view = scrubber;
}

NSScrubber 的 Delegate 和 Data Source 很类似于 Table View 的,直接看代码吧:

- (NSInteger)numberOfItemsForScrubber:(NSScrubber *)scrubber {
    return self.fontNames.count;
}

- (NSScrubberItemView *)scrubber:(NSScrubber *)scrubber viewForItemAtIndex:(NSInteger)index {
    NSScrubberTextItemView *view = [scrubber makeItemWithIdentifier:TextItemIdentifier owner:nil];
    view.textField.stringValue = self.fontNames[index];

    return view;
}

- (NSSize)scrubber:(NSScrubber *)scrubber layout:(NSScrubberFlowLayout *)layout sizeForItemAtIndex:(NSInteger)itemIndex {
    NSString *string = self.fontNames[itemIndex];
    NSRect bounds = [string boundingRectWithSize:NSMakeSize(CGFLOAT_MAX, CGFLOAT_MAX)
                         options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading
                      attributes:@{NSFontAttributeName: [NSFont systemFontOfSize:0]}];

    return NSMakeSize(bounds.size.width + 20, 30);
}

- (void)scrubber:(NSScrubber *)scrubber didSelectItemAtIndex:(NSInteger)selectedIndex {

}

注意你需要自己实现子视图的尺寸计算工作,在代理方法中计算文本尺寸,加上预留的 padding 返回给 NSScrubber

本文简单的介绍了一下 Touch Bar 的开发模式,实质上还是类似 Table View 一样,由 Data Source 和 Delegate 组成。很多属性大家可以自己尝试,本文就不再啰嗦了。