ちょっとした NSView の使い方

MacDev

AppKit はとにかく資料が少ない。世の中に出回っている情報は古く、最新情報は OS X の新バージョンが出るタイミングで発行される AppKit Release Notes ぐらいにしか書かれていなかったりするので、これを読んでいないとすぐに置いていかれてしまう。

今日は NSView の描画の話。「UIView みたいに backgroundColor プロパティがないから面倒だ」みたいな話をときどき見かけるけど、正しい方法を知っていればそこまで手間はかからない。

highlighted プロパティに応じて YES ならレッド、NO ならグレーの角丸矩形を描画する NSView サブクラスを考える。

@property (nonatomic, getter=isHighlighted) BOOL highlighted;

→ 続きを読む

知らずに回る Run Loop にご用心

MacDev

CotEditor 2.1.6 リリース!

以前記事を書いて紹介したように(→ 2015.2.19)何年も愛用している OS X 用プレーンテキストエディタである CotEditor。最新バージョンの 2.1.6 がリリースされた。

→ CotEditor -Text Editor for OS X

変更点を見てみると:

いくつか前のバージョンから保存時にフリーズするという問題があったのだけど、ほとんどの人はこれで直ると思います。

...という自慢でした。😤

-[NSTask waitUntilExit] の罠

自慢するだけなのもあれなので、今回の問題を引き起こしていた原因を同じバグで悩んでいる開発者のためにメモしておく。

CotEditor ではファイルの読み書きに authopen というコマンドを使うような構造になっているのだけど、それを Cocoa から呼び出すために NSTask というクラスが使われている。保存するデータをそこに投げたあと、処理を待つために呼ばれていたのが -waitUntilExit というメソッド。

waitUntilExit という名前の通り、アプリケーションの処理を休んでコマンドが終了するのを待ってくれるものかと思いきや、ヘッダファイルのコメントを読むと罠があった。

// poll the runLoop in defaultMode until task completes

Run Loop が回るらしい。...ということはこの待ち時間の間、先にセットされていたタイマーの時間が来たらその処理が次々と割り込んでくることになる!

CotEditor の場合、ファイルの保存で authopen が処理をしている中、その処理を待つ間に自動保存のタイマーが発動してしまい、予期せぬタイミングでもう一つの保存が実行されてしまっていた。それが結果的にフリーズを引き起こしていたのだ。

対策は簡単で、このメソッドを使わず単純なループで待つようにすればいい:

while (task.isRunning) usleep(200);

Wait 処理を使うときはどんな仕組みで待っているのか注意しましょう。

Yosemite の NSViewController はこれまでとは違う!

MacDev

iOS App を作っている人なら、UIViewController が必要不可欠な存在であることがわかるはず。View(ビューとその子孫ビューのいくつか)を管理して、Model との関係をつなぎ、そのまとまり全体の処理を統括する Controller であり UIKit における MVC の柱と言える。

Mac ではどうだろうか。もともと AppKit では、1 つの NSWindowController ごとに MVC の関係を形成していた。View はウインドウであり、それに含まれる子孫ビューも含む。

ウインドウ単位で 1 Controller しか用意していないようではそのクラスがあまりに巨大化してしまう懸念があるけど、OS X 初期では処理ごとにウインドウが独立していることが多かったので、当時はそこまで不便ではなかった。ドロワーもシートもパネルもみんな独立したウインドウである。

OS X が進化するにつれ、Mac アプリケーションでは 1 つのウインドウ内にすべての機能を詰め込むようなデザインのものが多くなってきた。ドロワーは使われなくなったし、パネルで用意していたような機能もサイドバーとしてウインドウ内に配置されるようになってきた。こうなるともっと処理を分散させたい。そこで、OS X 10.5 Leopard において追加されたのが NSViewController というクラス。待望の、ビュー単位の Controller。これでアプリケーション開発が楽になるかと思いきや、本当に使い物にならなかった。

中身がなさすぎた

Leopard の時点で使い物にならないのはしかたがないとして、4 回もバージョンを重ねた OS X 10.9 Mavericks になっても NSViewController の中身はほとんど変化せず

  • -initWithNibName:bundle:
  • -loadView
  • .representedObject
  • .nibBundle
  • .nibName
  • .view
  • .title
  • -commitEditingWithDelegate:didCommitSelector:contextInfo:
  • -commitEditing
  • -discardEditing

たったこれだけ。ほとんど nib でビューを読み込んで包み込むだけ。ビューからの必要な通知も受け取れないし、レスポンダチェーンとしてアクションを受け取ることもできない。Cocoa のほかのクラスからも無視された状態であり、NSViewController とのつながりがあったのは OS X 10.7 Lion で追加された NSPopover と OS X 10.8 Mountain Lion で追加された NSPageController ぐらいだった。

この状況は OS X 10.10 Yosemite の登場で大きく変わることになる。手を入れるのが遅すぎると言いたくなるけど、今回は本気のようだ。便利なプロパティやメソッドが増えただけではなく、Cocoa アプリケーションを構成する一員としてしっかりと組み込まれた。今回は AppKit Release Notes for OS X v10.10 を読みながら、その変更点を紹介してみようと思う。

とは言っても、iOS の UIViewController とほぼ同等のものになったというだけの話である。

→ 続きを読む

ユーザの操作として UITextView/NSTextView の値を書き換える方法

MacDev

テキストビューの値を書き換えるとき、Mac なら -[NSTextView setString:]、iOS なら -[UITextView setText:] を呼ぶと思う。

ビューが表示される前に初期値を設定するような用途ではこれだけでいいのだけど、ユーザの操作に対してこれを使うと不十分だったりする。

例えばクリアボタン。Timinder でも使ったけど、ユーザがテキストを入力したあと、素早く空に戻したいときにあると便利。
その実装が [textView setText: @""] だったとする。そのあとユーザが取り消したくなって本体を振っても、先ほど空にした操作は Undo Manager に登録されていないので取り消しできない。

自力で Undo Manager に操作を登録したりしてもいいけど、ユーザのキーボード入力と同じような扱いでコードから値を書き換える方法があれば、そちらの方が理想的。
NSTextView(Mac)での方法は知っていたけど、UITextView(iOS)の場合どうすればいいのかは最近までわからなかったのでメモしておく。

→ 続きを読む

iOS 7 キーカラーの落とし穴

MacDev

iOS 6 以前は、それぞれの UI 部品の挙動をユーザに想像&認識させるために光源と陰/影の再現による立体的な描写が用いられてきた。押せるものは立体的に膨らんでいるし、手前にある物体の背後には影がある。
これは現実世界における現象と同じで、人間の脳にもともと備わっている形状認識機能を活かすことができ、とても自然で合理的なやり方である。Mac OS や Windows など、PC の世界でも長いこと使われてきた手法。

iOS 7 では方針が大幅に変更された。ほとんどの UI 部品からは立体表現が消え、単純な線や塗りつぶしのみで表現されるようになった。ボタンなんて枠すらついていない。UI 部品がすっきりすればユーザがコンテンツに集中できるようになるはずだと。
でもそれだけではコンテンツ表示部と操作可能な部分の区別がつかないので、アイブ率いる新 UI チームが取った手法はキーカラー。App 内で一つのカラーを決めておき、ボタンのように押せるものはそれで塗りつぶす。人間が何かを探すときまず初めに認識するのは色なので、どこにボタンがあるのか、一瞬でわかる。

でも、この方法って無理がある。1 つのカラーに特定の役割を持たせるならコンテンツ内で似たようなカラーを使うとユーザの混乱の原因になるし、キーカラーの選択も難しい:

1. 鮮やかで濃いキーカラー
→ コンテンツよりも目立って邪魔になる(コンテンツと同列な見た目なので余計に)
2. 鮮やかで薄いキーカラー
→ 読みにくい、見にくい(iOS 7 は細いラインが多いので余計に)
3. くすんだキーカラー
→ ラベルと区別がつきにくくなる
4. 薄いキーカラー
→ 非アクティブ(Disabled 状態)に見えてしまう
5. ラベルのカラーと同じキーカラー
→ だめ!

上記の問題を解決しないままリリースされている App がいくつもある。特に 4 と 5 はよくない。5 をやるんだったら、せめて Windows Phone みたいに徹底的に ○ で囲むとか、何か区別をつけないと。

Mac アプリケーションが個性を出すべきではない部分

MacDev

Reeder が独自タイトルバーを採用しているように、ここ数年 Human Interface Guidelines を一部無視して作られたアプリケーションが人気になっているし、“The HIG is dead”みたいな話もときどき目にする。

確かに HIG はあらゆるタイプのアプリケーションが違和感なく共存できるように考えられていて、アプリケーションによっては無駄を感じられる部分もある。

それでも、いくつかの要素に関しては変えてしまうと個性というより違和感になってしまうので、個人的に気になるものをまとめてみる。

細かいことばかりに思われるかもしれないし、「こんなの UI デザインの本質ではない」とか言う人もいるかもしれない。だけどこういう“細かいひっかかり”だって積もれば山になるし、そんな部分が比較的少ないことが自分が Mac を使う理由でもある。

スクリーン座標と UI 部品のレイアウト

Mac では昔からウインドウ内の UI 部品が、左上から右下にかけてだいたい次のように並んでいる:

↖ [削除] [閉じる] [キャンセル] [前へ] [次へ] [完了] ↘

これが統一されている意味は大きく、ダイアログでキャンセルしたければテキストを確認しなくても自然にカーソルを左に動かすし、作業を次に進めたければ右下を探す。アプリケーションに慣れる時間を大幅に短縮できる。

記号とその意味

機械に慣れていない人でも CD プレーヤーを操作できるのは、▶ を押せば再生、止めるときには ■ を押せばいいと誰もが知っているからである。Mac の UI においても、同じ意味に対しては共通の記号を用いるべきだと思う。

Apple は履歴を移動するボタンを三角 [◀ | ▶] で統一しているし、そうでない移動(iPhoto など)には矢印 [← | →] が使われることが多い。

メニューを表示するボタンは ▼ をつけるべきという話があるけど、Apple 製アプリケーションも含め、つけないものが増えているのが残念。

UI における表記

記号と同じ。すべての項目を選択したいとき、メニューから“すべてを選択”を探す。これがアプリケーションによって“一括選択”とか“全部選択する”のようにバラバラだったら、探すのに時間がかかるだけで何のメリットもない。Windows には Windows の、OS X には OS X のための文字列を用意するべきである。

Mac アプリケーションの日本語リソースにおける法則については Mac Apps + Japanese というページにまとめてあるので、参考にどうぞ。

ユーザの操作に対する移動量

ユーザがトラックパッドやマウスで行った操作がどのように反映されるかは非常に重要で、プラットフォームの操作感を大きく左右する部分でもある。

ありがたいことにマウスカーソルの移動は完全にシステムが管理しているため、どのアプリケーションでも同じ。この速度や加速度がバラバラだったらと思うとぞっとする。

しかしピンチジェスチャとか 2 本指スクロールをしたときの移動量が違うアプリケーションはいくつかあり、非常に気持ち悪い(例:Photoshop CS6)。Photoshop は CS3 では自然なスクロールだったのになぜ変えてしまったんだ!

光源

ウインドウの影を見ればわかるように、OS 9 までは左上にあった光源が OS X では中心ちょっと上に移動している。グラデーションやドロップシャドウを見れば、メニューバーからボタンやアイコンまで、スクリーン上のすべてが共通の光源を持っているのがわかる。

全体の光源を無視して影を右下に置くようなアプリケーションは安っぽく見えてしまうので注意。

OS X 10.8 で改善された Layer Backed View とテキストレンダリング

MacDev

以前 NSView (AppKit) の CALayer 対応について、テキストのサブピクセルレンダリングが上手く機能しない問題があるという記事を書いた。Sleipnir for Mac の開発ブログ記事でもこの問題に触れられている。

復習すると、

  • 古くからの描画方式では通常 1 つのウインドウにつき 1 枚のグラフィックスコンテキストがある。
  • layer-backing を有効にするとそれぞれの NSView がグラフィックスコンテキストを持つようになる。
  • サブピクセルレンダリングは背景の色に依存。必ず背景の上に描画しないといけないので Layer-backed view とは相性が悪い。

...しかし、あれは Lion の時点での話。

Mountain Lion での改善

10.8 の AppKit Release Note にはこんなことが書かれている:

NSTextField has been updated to allow LCD font smoothing to work when the view is layer-backed. Prior to 10.8, NSTextField would directly draw the text into the layer's contents; this would cause text to render incorrectly due to the LCD font smoothing algorithm not having adjacent pixels to smooth fonts with. Layer-backed applications that manually draw text should move to using NSTextField to get proper LCD font smoothing.

自分の理解:

  • NSTextField がアップデートして layer-backed でもサブピクセルレンダリングできるようになった!
  • これまで NSTextField の描画内容は、ほかの NSView サブクラスと同様、layer の contents に 1 枚のビットマップ画像として設定されていた。これでは描画するときにその背景を知りようがないので正しくサブピクセルレンダリングできない。
  • そこで 10.8 以降は何か特別な仕組みで描画するようになったみたい。
  • 今後は、自作ビュー内に自力で描画していたテキストも NSTextField に任せるといいらしい。

続きを読むとこれが機能するには条件があって:

One caveat is that NSTextField does require an ancestor which is layer-backed and opaque; it does not have to be the direct parent view, but some view in the ancestor chain must be opaque for this to be turned on and work correctly.

NSTextField の superview をたどっていったとき、先祖のどこかで「layer-backed かつ opaque」なビューが必要らしい。

実験してみよう

いくつかの疑問:

  • 本当に機能するの?
  • 10.7 との違いは?
  • 裏にあるビューの中で色が変化しても大丈夫?

まず、NSWindow を 1 つ用意。contentView の中に NSTextField と SomeView を配置。

  • contentView:ウインドウ全体を覆うビュー。
    • 背景を [NSColor windowBackgroundColor] で塗りつぶす。
  • SomeView:適当な図形を表示するビュー。
    • -isOpaque で YES を返す。
    • 2 つ用意。上には NSTextField の兄弟として、下には NSTextField の親として配置。

結果 1:Layer なし

AppKit の昔からある描画方式。

言うまでもなくサブピクセルレンダリングは正しく機能している。これが理想。

結果 2:OS X 10.7.5 での結果

これ以降は contentView が layer-backed である。

字がふにゃふにゃしていて汚い。

結果 3:OS X 10.8.2 での結果

Release Note に書かれていた通りだ。

  • NSTextField と SomeView が兄弟関係にある上では効いていない。
  • 親子関係にある下ではしっかりサブピクセルレンダリングされている。

サブピクセルレンダリングが効いていない部分(上)も Lion(結果 2)より奇麗。

結果 4

おまけ。contentView を opaque にしてみた。

これで全体にサブピクセルレンダリングが効くようだ。結果 1 との違いもないのではないか。

結論

NSView が CALayer による layer-backing をサポートしていても使うのをあきらめてしまう原因の一つだったけど、Mountain Lion で大きく改善されたようだ。ちょっとした制約はあるけれども。