Swift で Touch Bar 開発 - 今更だけど寿司を回す
なぜか SubView の Basic Animation が動かなくなった。
修正したコードを貼っておく。
前回のものがそのままでは動かなくなってしまい、そのまま直すのあれなので、今更だが寿司を回すことにする。
あと、前回すっ飛ばした概要の説明。
今更だけど Touch Bar とは
宇宙に衝撃を与えるため、
ESC、ファンクションキー、電源ボタンを過去のものにして代わりにタッチセンサーを載せたもの。
アプリケーションの状態によって適切な機能が表示され、使用できるようになっている。
(物理の ESC キー返せ)
キーボードの拡張としてつくられたものなので、アプリケーションの表示上にない機能は実装してはならないらしい。
例外として、フルスクリーン時にクリックや右クリックがされない限り操作でにないものは、
画面上に表示されていなくても使用可能とのこと。
よって、残念ながら今回のタッチバーの機能は実装してもアプリの審査を通ることはない。
Touch Bar 動作
NSTouchBar クラスを呼び、デリゲートから NSTouchBarItem をぶち込むだけだ。
NSTouchBarItem は大概カスタムの NSCustomTouchBarItem で、
ボタンなどの UI や ViewController をぶち込む。
ちゃんとした説明
Touch Bar は NSTouchBar オブジェクトであり、NSTouchBarItem の配列である。
でもって、NSTouchBar オブジェクトは以下の条件を満たす必要がある。
- キーボードの拡張らしいのでレポンダーチェインが使用されており、NSResponder のサブクラスのインスタンスである
- NSTouchBarProvider プロトコルに準拠する
- そのプロトコル内で makeTouchBar() メソッドを実装する
AppKit のほとんどのレスポンダーは、
NSTouchBarProvider プロトコルに準拠し、
Key-Value Observing (KVO) をサポートしていて、
キーの値の変更で通知し、delegate に書かれた振る舞いで Touch Bar の中身が変わる。
どうやら NSTouchBarProvider プロトコルに準拠するオブジェクトを Bar provider というらしく、
Bar provider が持つ touchBar プロパティ に nil を渡すと makeTouchBar() -> NSTouchBar? が動き、
NSTouchBar を設定値とともに返り値として、touchBar プロパティに渡す。
touchBar プロパティはデフォルトで空なので、
makeTouchBar() が呼ばれるが何もないので、
Touch Bar は何も表示されていない状態になる。
と、書いたが、
NSResponder も NSTouchBarProvider も makeTouchBar() も組み込まれているので、
とりあえず、つくる上では今回はあんまり深く考えなくてもよい。
今回の環境
Touch Bar がない場合はエミュレータを使って開発が可能。
Xcode を立ち上げてメニューバー Window > Show Touch Bar で表示される
下準備
プロジェクト作成
macOS で Cocoa Application を選択し、「Next」ボタンを押す。
ファイル名を入力。
ちなみに、Swift を使うので Language は「Swift」を選択。
問題がなければ、「Next」ボタンを押す。
保存先を選んでプロジェクト作成完了。
NSWindowController 的なクラスをつくる
今回は真面目につくる。
Command + N を押して新しいファイルをつくり、
Cocoa Class を選択して「Next」ボタンを押す。
Class 名はなんでもよいが、とりあえずここでは「WindowController」に。
でもって、サブクラスを NSWindowController を選択。
プロジェクト作成時の Main.Storyboard を使用するので、
「Also create xib file for user interface」チェックボックスを外し、
「Next」ボタンを押す。
つくったクラスを Storyboard で紐付ける
Main.Storyboard 開き
左側 Document Outline の「Window Controller Scene」を選択
Command + Control + 3 を押して、右側の Identity Inspector
最初の項目 Custom Class の Class の下矢印から
先ほど作成した「WindowController」を選び変更。
これで下準備完了。
とりあえず、Touch Bar に寿司を表示させてみる
まず、ESC キーの横に寿司を1つ表示させるよ。
はじめに Touch Bar に表示させる ViewController を作成から。
NSWindowController の時と同様に、 Command + N を押して新しいファイルをつくり、 Cocoa Class を選択して「Next」ボタンを押す。
Class 名はなんでもよいが、とりあえずここでは「SushiController」に。 でもって、サブクラスを NSViewController を選択。 プロジェクト作成時の Main.Storyboard を使用するので、 「Also create xib file for user interface」チェックボックスを外し 「Next」ボタンを押す。
SushiController.swift を開いて書いていく。
viewDidAppear() で Touch Bar 表示させるのだが、 loadView() で self.view を設定しないと怒られるので、 super.viewDidLoad() の下に self.view = NSView() を書く
override func loadView() { super.viewDidLoad() self.view = NSView() }
そんでもって、Touch Bar 中身である viewDidAppear() の中を書いていく。
流れ
- 表示するための NSView をつくる
- NSView の大きさと位置を決める
- SushiController の view に addSubview で作成した NSView を入れる
- NSTextView に絵文字の寿司を設定する
- NSView に addSubview で作成した NSTextView を入れる
以下、コード。
override func viewDidAppear() { let sushiView = NSView() sushiView.frame = CGRect(x: 0, y: 0, width: self.view.frame.width, height: self.view.frame.height) sushiView.layer?.position = CGPoint(x: 0, y: 0) self.view.addSubview(sushiView) let sushi = NSTextView(frame: NSRect(x: 0, y: -2, width: 30, height: 30)) sushi.string = "🍣" sushi.drawsBackground = false sushi.font = NSFont.systemFont(ofSize: 20) sushiView.addSubview(sushi) }
あ、寿司は TextView で表示しているので drawsBackground で false を設定しないと
寿司の背景が白くなるので気をつけて。
とりあえず、Touch Bar 中身はつくったのだが、 表示する設定はしていないので、
続いて書いて行く
NSWindowController.swift をいじっていく。
もう忘れている可能性があるが、最初の方に説明している動作を実装する。
流れ
- 表示する NSTouchBarItem の識別子をつくる
- デフォルトで呼び出される makeTouchBar() をオーバーライドして NSTouchBar や識別子などを設定する
- Touch Bar のデリゲートで識別子からカスタムの TouchBarItem を設定する
この3つを設定するを表示できる
てことで、以下コード。
まず、初めに
import Cocoa の下に Touch Bar 表示させるための識別子を設定する。
とりあえず、以下のコードを書く
fileprivate extension NSTouchBarItemIdentifier { static let sushiID = NSTouchBarItemIdentifier("jp.food.touchbar.sushi") }
NSTouchBarItemIdentifier を拡張して NSTouchBarItemIdentifier を設定。
今回は一つしかないが複数ある場合は、被らないような文字列に。
ちなみに、fileprivate というアクセス修飾子は Swift 3.0 から導入されたもので
そのファイル内でしか使用できない。
今回の場合だと NSTouchBarItemIdentifier.sushiID は NSWindowController.swift でしか使用できない。
試しに、SushiController.swift に 「NSTouchBarItemIdentifier.s」まで打ち込んでも入力候補に出てこないはずだ。
次は override func windowDidLoad() { ... } の下に 以下の初期設定のコードを入力
@available(OSX 10.12.2, *) override func makeTouchBar() -> NSTouchBar? { let mainBar = NSTouchBar() mainBar.delegate = self mainBar.defaultItemIdentifiers = [.sushiID] return mainBar }
NSTouchBar を設定し、デリゲートとアイテムの識別子を設定し NSTouchBar を返す。
識別子は先ほど設定したものを配列として入れている。
ちょっと前に書いたように、オーバーライドした makeTouchBar() の返り値 NSTouchBar は WindowController が持つ
self.touchbar プロパティ渡される。
self.touchbar に自前の NSTouchBar を返す関数を放り込むことで Touch Bar の表示を変えることができる。
ちなみに @available(OSX 10.12.2, *) は見たままだが、
これを書いた下のブロックは macOSX 10.12.2 以降で動作するというやつ。
続いてデリゲートの設定
class WindowController {} の下に
以下のコードを書く
@available(OSX 10.12.2, *) extension WindowController: NSTouchBarDelegate { func touchBar(_ touchBar: NSTouchBar, makeItemForIdentifier identifier: NSTouchBarItemIdentifier) -> NSTouchBarItem? { if identifier == .sushiID { let item = NSCustomTouchBarItem(identifier: identifier) item.viewController = SushiController() return item } return nil } }
前回同様、WindowController を拡張してデリゲートを書いている。
内容は識別子が sushiID だったら NSCustomTouchBarItem を設定し、
viewController の中身を最初につくった SushiController() にしている。
入力した WindowController.swift は 以下のようになる。
import Cocoa fileprivate extension NSTouchBarItemIdentifier { static let sushiID = NSTouchBarItemIdentifier("jp.food.touchbar.sushi") } class WindowController: NSWindowController { override func windowDidLoad() { super.windowDidLoad() } @available(OSX 10.12.2, *) override func makeTouchBar() -> NSTouchBar? { let mainBar = NSTouchBar() mainBar.delegate = self mainBar.defaultItemIdentifiers = [.sushiID] return mainBar } } @available(OSX 10.12.2, *) extension WindowController: NSTouchBarDelegate { func touchBar(_ touchBar: NSTouchBar, makeItemForIdentifier identifier: NSTouchBarItemIdentifier) -> NSTouchBarItem? { if identifier == .sushiID { let item = NSCustomTouchBarItem(identifier: identifier) item.viewController = SushiController() return item } return nil } }
Command + R を押すとビルドされ、アプリ起動後に、
Touch Bar の ESC キーの横に寿司が表示される。
寿司を並べてみる
ただ複数配置するだけ。
SushiController.swift を開いて再度編集
こちらを
let sushi = NSTextView(frame: NSRect(x: 0, y: -2, width: 30, height: 30)) sushi.string = "🍣" sushi.drawsBackground = false sushi.font = NSFont.systemFont(ofSize: 20) sushiView.addSubview(sushi)
こちらに変更
for i in 0...10 { let sushi = NSTextView(frame: NSRect(x: i * 80, y: -2, width: 30, height: 30)) sushi.string = "🍣" sushi.drawsBackground = false sushi.font = NSFont.systemFont(ofSize: 20) sushiView.addSubview(sushi) }
Command + R を押すとビルドされ、アプリ起動後に、
寿司がたくさん表示される。
寿司を動かしてみる
ただ Core Animetion で横移動するだけ。
SushiController.swift を開いて再度編集
先ほどループさせたブロックの後に以下のコードを追加。
let anim = CABasicAnimation(keyPath: "position") anim.repeatCount = .infinity anim.duration = 1 anim.fromValue = sushiView.layer?.position anim.toValue = NSValue(point: NSPoint(x: -80, y: 0)) sushiView.layer?.add(anim, forKey: "position")
アニメーションの種類は移動で、リピートは無限、1秒で -80px 左にアニメーションする。
このままだと、アニメーションが途切れるので、sushiView.frame の 幅を +80 する。
以下、SushiController.swift の全コード。
import Cocoa class SushiController: NSViewController { override func loadView() { self.view = NSView() } override func viewDidAppear() { let sushiView = NSView() sushiView.frame = CGRect(x: 0, y: 0, width: self.view.frame.width + 80, height: self.view.frame.height) sushiView.layer?.position = CGPoint(x: 0, y: 0) self.view.addSubview(sushiView) for i in 0...10 { let sushi = NSTextView(frame: NSRect(x: i * 80, y: -2, width: 30, height: 30)) sushi.string = "🍣" sushi.drawsBackground = false sushi.font = NSFont.systemFont(ofSize: 20) sushiView.addSubview(sushi) } let anim = CABasicAnimation(keyPath: "position") anim.repeatCount = .infinity anim.duration = 1 anim.fromValue = sushiView.layer?.position anim.toValue = NSValue(point: NSPoint(x: -80, y: 0)) sushiView.layer?.add(anim, forKey: "position") } }
Command + R を押すとビルドされ、アプリ起動後に、 寿司が回る。
おまけ
Command + W などで
アプリのウインドウを閉じてもアプリは終了しないので
class AppDelegate にその処理を追加する。
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true }
おまけ2
今回ウインドウに Touch Bar を設定したため、Dock にぶち込んで戻すと Touch Bar に以前のものが表示されつつ、新しいアニメーションが表示されてしまう。
なので、見えなくなったら初期化するコードを SushiController へ書く。
override func viewDidDisappear() { self.view = NSView() }
AppDelegate.swift で Touch Bar の処理を書くと上の処理はいらない。
self.touchBar の self を指す場所が NSApplication になるためだが、Apple のサンプルコードや Apple 謹製のアプリの振る舞いを見ていると NSWindow に touchBar プロパティを設定しているっぽい。
まとめ
以上、寿司を回してみた。
Touch Bar に NSViewController を表示させているため、かなりの自由なことができるようになっている。