Touch Bar 快速开发入门

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

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

本文要实现的效果如下:


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 组成。很多属性大家可以自己尝试,本文就不再啰嗦了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,185评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,652评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,524评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,339评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,387评论 6 391
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,287评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,130评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,985评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,420评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,617评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,779评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,477评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,088评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,716评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,857评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,876评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,700评论 2 354

推荐阅读更多精彩内容

  • *7月8日上午 N:Block :跟一个函数块差不多,会对里面所有的内容的引用计数+1,想要解决就用__block...
    炙冰阅读 2,486评论 1 14
  • 本文翻译自 raywenderlich.com 的 How to Use NSTouchBar on macOS,...
    SR2k阅读 1,954评论 0 2
  • /* UIViewController is a generic controller base class th...
    DanDanC阅读 1,811评论 0 2
  • 7、不使用IB是,下面这样做有什么问题? 6、请说说Layer和View的关系,以及你是如何使用它们的。 1.首先...
    AlanGe阅读 673评论 0 1
  • 2014年,高中毕业就很想去厦门,没去成。2017年,遇到一些事情,下定决心出来散心。没有去很多景点,也没有吃很多...
    fl0wery阅读 1,291评论 14 42