[Xcode 8.1] サンプルコードからTouchBarのAPIを理解する#5 NSTouchBar CatalogのCustom Viewの構造と処理
本記事の目標: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
を設定する」
です。