Apple Engine

Apple, iPhone, iOS, その周辺のことについて

Swift で Touch Bar 開発 - 今更だけど寿司を回す

なぜか SubView の Basic Animation が動かなくなった。
修正したコードを貼っておく。

github.com

 

前回のものがそのままでは動かなくなってしまい、そのまま直すのあれなので、今更だが寿司を回すことにする。

f:id:x67x6fx74x6f:20170124005816p:plain

あと、前回すっ飛ばした概要の説明。

 

今更だけど 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 で表示される

 

下準備

プロジェクト作成

macOSCocoa Application を選択し、「Next」ボタンを押す。

f:id:x67x6fx74x6f:20170124005940p:plain

ファイル名を入力。
ちなみに、Swift を使うので Language は「Swift」を選択。
問題がなければ、「Next」ボタンを押す。

f:id:x67x6fx74x6f:20170124010644p:plain

保存先を選んでプロジェクト作成完了。

 

NSWindowController 的なクラスをつくる

今回は真面目につくる。
Command + N を押して新しいファイルをつくり、 Cocoa Class を選択して「Next」ボタンを押す。

f:id:x67x6fx74x6f:20170124010728p:plain

Class 名はなんでもよいが、とりあえずここでは「WindowController」に。
でもって、サブクラスを NSWindowController を選択。

f:id:x67x6fx74x6f:20170124010759p:plain

プロジェクト作成時の 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」を選び変更。

これで下準備完了。

f:id:x67x6fx74x6f:20170124010908p:plain

 

とりあえず、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」ボタンを押す。

f:id:x67x6fx74x6f:20170124011101p:plain

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 キーの横に寿司が表示される。

 

f:id:x67x6fx74x6f:20170124012011p:plain

 

寿司を並べてみる

ただ複数配置するだけ。

 

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 を押すとビルドされ、アプリ起動後に、
寿司がたくさん表示される。

 

f:id:x67x6fx74x6f:20170124015218p:plain

 

寿司を動かしてみる

ただ 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 を表示させているため、かなりの自由なことができるようになっている。