storyboardの中のNSWindow initialFirstResponderが効かない問題について (2)

表題について、最も簡単な方法は、有効にしたいviewControllerのawakeFromNibの中で、initialFirstResponderの設定をする事です。

awakeFromNibが複数回呼ばれる事にさえ気をつければ最も簡単な解決方法になります。

@interface ViewController : NSViewController

@property (weak) IBOutlet NSTextField* textField;

@end

@implementation ViewController

- (void) awakeFromNib
{
    [super awakeFromNib];

    // 汎用性はないが、もっと簡単な方法
    self.view.window.initialFirstResponder = self.textField;
}

@end

この解決法の問題点は、汎用性がない事です。「storyboardの中のNSWindow initialFirstResponderが効かない」のはnibファイル間でoutletが接続できない事が原因です。

ここでは、このnibファイル間でoutlet接続をする方法を解説します。

nibファイルの読み込み時に何が起こっているのか?

まずは前提条件となる知識として、nibファイルの読み込み時に起こっていることを説明します。

nibファイルの読み込み時には以下のようなことがおこります。

  1. 非カスタムオブジェクトのアンアーカイブ
フラット化されたストリームからNSKeyedArchiverが作成され、そこから初期化メソッドとしてinitWithCoder:が呼ばれ、それぞれのObjectが生成されます。
  1. カスタムオブジェクトのインスタンス化 (サロゲートObject) cutumViewなど
カスタムViewなどのdecodeされたわけではないviewが、初期化メソッドinitWithFrameが呼ばれて、
  1. オブジェクト間のコネクション関連の設定やRuntimeAttibuteの設定
IBOutletConnectionのサブクラスにestablishConectionメッセージが呼ばれ、コネクションを確立します。
  1. nibから再生された全てのオブジェクトにawakeFromNibを呼び出し
objectの生成もコネクションの確立も完了したのちに、awakeFromNibを呼び出してそれぞれのオブジェクトに追加の初期化処理への機会を与えます。

正確ではないかもしれませんが、以上のような手順でnibから読み込まれます。

Note

なお、ストーリーボードが複数のnibに分割される場合には、3と4の間でまた別のnibを読み込むことがあります。そして、この分割によってoutletが接続できないのです。困ったね。

nibファイル間のoutlet接続の実現のアイデア

outlet接続では、objectを直接outletの変数接続している。 これはアーカイブしたときには、objectのハッシュ値になるわけだが、アンアーカイブされたときにobjectのポインター値へ変換される。

同じ、接続(IBConnectors)プロトコルをサポートしているものでも、bindingの様に、ポインターとパスの組み合わせでobjectを指定するものがある。

今回は、このbinding接続の様にパスを使うことで、目的のobjectへ到達できる様にすることを目標にクラスを作成してた。

ただし、現状のIBOutletConnectionのサブクラスを作成してもXCode場では使用できないので、NSObjectのサブクラスを作成することにした。

実際の接続作業は、nibファイル中のobjectが全てインスタンス化した後のawakeFromNibメソッドの中で行う。

要約すると、

  • 起点とするrootとなるobjectへのポインターを設定
  • 起点から接続先を示せるキーパス文字列を設定
  • Outletが設定されているobjectはポインターで設定
  • Outletの変数名は、キー名で設定
  • 上記の値を元に、awakeFromNibでコネクションを確立する。
  • IBOutletConnectionをサブクラス化しない理由は、XCode場で編集できないから

クラスの実装

ヘッダファイルはこんな感じ。

@interface MTLOutletProxy : NSObject

// オブジェクトoutletObjectのアウトレット名outletNameにポインター値を入れる。
@property (weak) IBOutlet      id        outletObject;
@property (copy) IBInspectable NSString* outletName;

// そのポインター値は、objectRootを起点として、キーパスobjectPathで示されるオブジェクトのポインター
@property (weak) IBOutlet      id        objectRoot;
@property (copy) IBInspectable NSString* objectPath;

@end

実装はこれだけ。

@implementation MTLOutletProxy

- (void) awakeFromNib
{
    [self.outletObject setValue:[self.objectRoot valueForKeyPath:self.objectPath]
                     forKeyPath:self.outletName];
}

@end

以上の様に非常にシンプルなクラスになる。

クラスの使用方法

「storyboardの中のNSWindow initialFirstResponderが効かない」の問題を、このクラスを用いて解決する。

  1. ここにMTLOutletProxyを置く
NSObjectを配置して、class名をMTLOutletProxyに変更する。
../../../_images/MTLOutletProxy_1.png
  1. viewControllerにキーパスでタグれる様にプロパティを作っておき、outlet接続しておく。
コードは以下の通りですが、普通はイニシャルレスポンダーにしたいobjectへはすでにoutletがあるはず。
@interface ViewController : NSViewController

@property (weak) IBOutlet NSTextField* textField_1;
@property (weak) IBOutlet NSTextField* textField_2;
@property (weak) IBOutlet NSTextField* textField_3;
@property (weak) IBOutlet NSTextField* textField_4;//<--今回はこれをイニシャルファーストレスポンダーにする

@end
  1. Windowと接続
MTLOutletProxyのoutletObjectアウトレットに、NSWindowを指定する。
../../../_images/MTLOutletProxy_2.png
  1. 起点としてWindowControllerと接続
MTLOutletProxyのobjectRootアウトレットに、NSWindowControllerを指定する。 キーパスの起点になります。
../../../_images/MTLOutletProxy_3.png
  1. Windowの中のinitialFirstResponderプロパティを指定するために文字列を設定
outlet nameにinitialFirstResponderを入力
../../../_images/MTLOutletProxy_4.png
  1. WindowControllerからキーパス文字列を設定して、最初にフォーカスが当たるObjectを指定
object pathにcontentViewController.textField_4を入力
../../../_images/MTLOutletProxy_5.png

実行時に起こること

MTLOutletProxyの以下のメソッドが呼び出される。

- (void) awakeFromNib
{
    [self.outletObject setValue:[self.objectRoot valueForKeyPath:self.objectPath]
                     forKeyPath:self.outletName];
}

先ほどの各種設定をして入れば、

window.initialFirstResponder = windowController.contentViewController.textField_4;

を実行しているのと同じになる。 このawakeFromNibの実行時点ではwindowは表示される前なのでinitialFirstResponderの値は有効になり、 textField_4がfirstResponderになる。

ただし、その後”Restore state”の機能が実行されて、2回目以降は前回のfirestResponderへフォーカスが移動してしまう。

Note

デバッグ時にRestore state機能をOffにするには、”Edit scheme…” > “Run” > “Options” > “Persistant State”のチェックボックスをONにする。 これを行わないと、最後にWindowを閉じた時のfirstResponderにフォーカスが当たってしまう。

以上で終わりです。じゃあね。