[Xcode 8.1] サンプルコードからTouchBarのAPIを理解する#5 NSTouchBar CatalogのCustom Viewの構造と処理
この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
本記事の目標:Custom Viewのサンプルを理解する
こんにちは!モバイルアプリサービス部の加藤潤です。
前回に引き続きNSTouchBar Catalogを理解していきます。
今回はサンプルアプリの左側に表示されるメニューの内、Custom Viewがどういう構造で成り立っているかを見ていきます。
実行結果
まず先にアプリとタッチバーの実行結果をご覧ください。
Custom View

最初は「Using Touch Events」が選択された状態となっており、バーの部分が青色になっています。
左右にドラッグすると、何やら座標らしき数値が変わります。
「Using Gesture Recognizers」を選択するとバーの色がグレーとなり、こちらも左右にドラッグすると座標らしき数値が変わります。
Storyboardの構造
CustomViewViewController.storyboardの中がどういう構造になっているかを見てみましょう。

アプリのビューがあり、「Using Touch Events」と「Using Gesture Recognizers」を選択した時のイベントとアクションメソッドの紐付けはされていますが、タッチバーがどこにも見当たりません。Storyboardでタッチバーを定義していないということはコードで定義しているのでしょう。
ソースコード
CustomViewViewController.swift
というわけでコードを見てみましょう。
CustomViewViewController.swiftは以下のようになっています。
import Cocoa
fileprivate extension NSTouchBarCustomizationIdentifier {
static let customViewBar = NSTouchBarCustomizationIdentifier("com.TouchBarCatalog.customViewBar")
}
fileprivate extension NSTouchBarItemIdentifier {
static let touchEvent = NSTouchBarItemIdentifier("com.TouchBarCatalog.TouchBarItem.touchEvent")
static let panGR = NSTouchBarItemIdentifier("com.TouchBarCatalog.TouchBarItem.panGR")
}
fileprivate enum InteractionTypeButtonTag: Int {
case touchEvent = 1000, panGR = 1001
}
class CustomViewViewController: NSViewController {
@IBOutlet weak var feedbackLabel: NSTextField!
var selectedItemIdentifier: NSTouchBarItemIdentifier = .touchEvent
// MARK: NSTouchBar
override func makeTouchBar() -> NSTouchBar? {
let touchBar = NSTouchBar()
touchBar.delegate = self
touchBar.customizationIdentifier = .customViewBar
touchBar.defaultItemIdentifiers = [selectedItemIdentifier]
touchBar.customizationAllowedItemIdentifiers = [selectedItemIdentifier]
return touchBar
}
// MARK: Action Functions
@IBAction func choiceAction(_ sender: AnyObject) {
guard let button = sender as? NSButton,
let choice = InteractionTypeButtonTag(rawValue:button.tag) else { return }
switch choice {
case .touchEvent:
selectedItemIdentifier = .touchEvent
case .panGR:
selectedItemIdentifier = .panGR
}
touchBar = nil
}
// MARK: Gesture Recognizer
func panGestureHandler(_ sender: NSGestureRecognizer?) {
guard let currentItem = self.touchBar?.item(forIdentifier: selectedItemIdentifier),
let itemView = currentItem.view, let panGR = sender else { return }
var feedbackStr = "Pan Gesture: "
let state = sender!.state
switch state {
case .began:
feedbackStr += "Began"
case .changed:
feedbackStr += "Changed"
case .ended:
feedbackStr += "Ended"
default:
break
}
let location = panGR.location(in: itemView)
feedbackStr += String(format: " {x = %3.2f}", location.x)
feedbackLabel.stringValue = feedbackStr;
}
deinit {
feedbackLabel.unbind(NSValueBinding)
}
}
// MARK: NSTouchBarDelegate
extension CustomViewViewController: NSTouchBarDelegate {
func touchBar(_ touchBar: NSTouchBar, makeItemForIdentifier identifier: NSTouchBarItemIdentifier) -> NSTouchBarItem? {
switch identifier {
case NSTouchBarItemIdentifier.touchEvent:
let canvasView = CanvasView()
canvasView.wantsLayer = true
canvasView.layer?.backgroundColor = NSColor.blue.cgColor
canvasView.allowedTouchTypes = .direct
feedbackLabel.unbind(NSValueBinding)
feedbackLabel.bind(NSValueBinding, to: canvasView, withKeyPath: #keyPath(CanvasView.trackingLocationString))
let custom = NSCustomTouchBarItem(identifier: identifier)
custom.view = canvasView
return custom
case NSTouchBarItemIdentifier.panGR:
let view = NSView()
view.wantsLayer = true
view.layer?.backgroundColor = NSColor.gray.cgColor
let panGestureRecognizer = NSPanGestureRecognizer()
panGestureRecognizer.target = self
panGestureRecognizer.action = #selector(panGestureHandler(_:))
panGestureRecognizer.allowedTouchTypes = .direct
view.addGestureRecognizer(panGestureRecognizer)
let custom = NSCustomTouchBarItem(identifier: identifier)
custom.view = view
return custom
default:
return nil
}
}
}
24〜32行目
makeTouchBar()をオーバーライドしてタッチバーを生成して返しています。defaultItemIdentifiersにはプロパティで保持しているselectedItemIdentifierを設定しています。selectedItemIdentifierの初期値はtouchEventとなっています。
NSTouchBarItemIdentifierとして他にpanGRも定義されているので、タッチバーアイテムとしてはタッチイベント用とパンジェスチャー用の2つありますね。
87行目以降
NSTouchBarDelegateのtouchBar(_:makeItemForIdentifier:)を実装し、パラメータで渡ってくるNSTouchBarItemIdentifierに対応したNSTouchBarItem(のサブクラス)を返しています。
touchEventかpanGRによって分岐していますが、どちらもNSCustomTouchBarItemを生成して返している点は同じです。
NSCustomTouchBarItemはviewプロパティに任意のNSViewを設定することができるためカスタムビューを表示したい場合に使えます。
touchEventの場合は後述するNSViewのサブクラスであるCanvasViewをviewプロパティに設定しています。
panGRの場合はNSPanGestureRecognizerを割り当てたNSViewをviewプロパティに設定しています。
ちなみに、このmakeTouchBar()のオーバーライドとNSTouchBarDelegateについてはPart.1でも出てきました。
53〜78行目
NSPanGestureRecognizerのハンドラーです。ここではタッチバーアイテムのViewの中のx座標を取得してNSTextFieldに出力しています。
36〜49行目
「Using Touch Events」または「Using Gesture Recognizers」を選択した時のアクションメソッドです。どちらが選択されたかを取得してselectedItemIdentifierプロパティを更新しています。
1つの疑問
さて、ここで1つ疑問が出て来ます。先ほど、makeTouchBar()の中でdefaultItemIdentifiersにプロパティで保持しているselectedItemIdentifierを設定しているとお伝えしました。ということはつまり、更新したselectedItemIdentifierを反映するためにはもう一度makeTouchBar()が呼ばれる必要がありますね。でもどうやってもう一度呼ばれるようにしているのでしょうか?
その答えは48行目のtouchBar = nilです。
Instance Property - touchBarに以下の説明があります。
If you have not explicitly provided an NSTouchBar object for a responder by setting this property, the system sends the makeTouchBar() message to the responder to create the default bar. This property is archived.
このプロパティが設定されていない場合はシステムがmakeTouchBar()を呼ぶようです。
「Using Touch Events」または「Using Gesture Recognizers」を選択した時にタッチバーのアイテムが切り替わっていたのはそういうからくりでした。
CanvasView.swift
参考までに上述したCanvasViewのソースコードも載せておきます。
こちらはtouchesBegan(with:)などでタッチイベントをハンドリングして自身のビュー内のx座標を出力しています。
class CanvasView: NSView {
// This is to keep the current tracking touch (figure).
// The Touch Bar supports multiple touches but since it has only 30-point height,
// it is reasonable to only track one touch.
var trackingTouchIdentity: AnyObject?
// Marked as dynamic for this property to support KVO.
dynamic var trackingLocationString = ""
// NSView by default doesn't accept first responder, so override this to allow it.
override var acceptsFirstResponder: Bool { return true }
override func touchesBegan(with event: NSEvent) {
// trackingTouchIdentity != nil:
// We are already tracking a touch, so ignore this new touch.
if trackingTouchIdentity == nil {
if let touch = event.touches(matching: .began, in: self).first, touch.type == .direct {
trackingTouchIdentity = touch.identity
let location = touch.location(in: self)
trackingLocationString = String(format: "Began at: {x = %3.2f}", location.x)
}
}
super.touchesBegan(with: event)
}
override func touchesMoved(with event: NSEvent) {
if let trackingTouchIdentity = self.trackingTouchIdentity {
if let trackingTouch = event.touches(matching: .moved, in: self).filter({
$0.type == .direct && $0.identity.isEqual(trackingTouchIdentity)}).first {
let location = trackingTouch.location(in: self)
trackingLocationString = String(format: "Moved at: {x = %3.2f}", location.x)
}
}
super.touchesMoved(with: event)
}
override func touchesEnded(with event: NSEvent) {
if let trackingTouchIdentity = self.trackingTouchIdentity {
if let trackingTouch = event.touches(matching: .ended, in: self).filter({
$0.type == .direct && $0.identity.isEqual(trackingTouchIdentity)}).first {
self.trackingTouchIdentity = nil
let location = trackingTouch.location(in: self)
trackingLocationString = String(format: "Ended at: {x = %3.2f}", location.x)
}
}
super.touchesEnded(with: event)
}
override func touchesCancelled(with event: NSEvent) {
if let trackingTouchIdentity = self.trackingTouchIdentity {
if let trackingTouch = event.touches(matching: .cancelled, in: self).filter({
$0.type == .direct && $0.identity.isEqual(trackingTouchIdentity)}).first {
self.trackingTouchIdentity = nil
let location = trackingTouch.location(in: self)
trackingLocationString = String(format: "Canceled at: {x = %3.2f}", location.x)
// The touch will be cancelled, so roll back to the status before touchBegan
//...
}
}
super.touchesCancelled(with: event)
}
}
まとめ
今回は前回と異なり、タッチバーをStoryboardで定義せず、コードで定義する方法を学びました。 やはりコードで定義する方が「その時々のアプリのコンテキストに応じたタッチバーを表示する」という観点ですと色々と融通がきくのではないかと思います。
今回のポイントはズバリ!
「makeTouchBar()でタッチバーを再生成するためにはtouchBarプロパティにnilを設定する」
です。









