nib、よくわからないまま使っていませんか?
そんなnibのこと、知ってみませんか?
Nib
と Xib
って何が違うの?
nib = NeXT Interface Builder
のアクロニム。Interface Builder version 3 以前では拡張子 .nib
が使われていたらしい。
xib = Interface Builder version 3 で追加されたファイル形式。フラットなファイル(XML)なので xib
。
らしい。
ソースは
https://stackoverflow.com/questions/3726400/what-is-the-difference-between-nib-and-xib-interface-builder-file-formats
https://en.wikipedia.org/wiki/Interface_Builder#Design
ロードするとき Bundle
と UINib
のどっち使えばいいの?
class HogeView: UIView {
// ①
func instantiateFromBundle() {
let bundle = Bundle(for: type(of: self))
let view = bundle.loadNibNamed("HogeView", owner: self, options: nil)!.first as! UIView
...
}
// ②
func instantiateFromNib() {
let bundle = Bundle(for: type(of: self))
let nib = UINib(nibName: "HogeView", bundle: bundle)
let view = nib.instantiate(withOwner: self, options: nil).first as! UIView
...
}
}
①も②も、動作するコードです。
NSBundle
( Bundle
)を使うこともできるし、 UINib
からロードすることもできる。一体何が違うのかというと…
何度もインスタンス化するなら UINib
を使いましょう。 とドキュメントに明記されています。
A UINib object caches the contents of a nib file in memory, ready for unarchiving and instantiation. When your application needs to instantiate the contents of the nib file it can do so without having to load the data from the nib file first, improving performance. (...) Your application should use UINib objects whenever it needs to repeatedly instantiate the same nib data.
「 UINib
オブジェクトはコンテンツをメモリキャッシュし、unarchivingとインスタンス化に備えます。nibを何度もインスタンス化するのであれば、 UINib
を使うべきです」(ざっくり訳)とのことです。 NSBundle
を使う場合、毎回ディスクに読みにいってしまうのでオーバーヘッドがあるけれど、それを一回で済ませてメモリキャッシュしてくれるのが UINib
の役割なのですね。
つまり先述のコード例では、bundle.loadNibNamed(...)
しているほうはパフォーマンス的に良くないコードです。
避けましょう。
nib のロード時に owner: self
渡してるけど、これって何やってるの?
初めて見るときには、奇妙に感じますよね。このコード。
final class HogeView: UIView {
@IBOutlet weak var label: UILabel!
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!
instantiateFromNib()
}
func instantiateFromNib() {
let nib = UINib(nibName: "HogeView", bundle: Bundle(for: type(of: self)))
let view = nib.instantiate(withOwner: self, options: nil).first as! UIView
// 👆 なんで `self` を渡してるの?
self.addSubview(view)
// 👆 なんで `addSubview` しなきゃいけないの?
view.translatesAutoresizingMaskIntoConstraints = false
view.topAnchor.constraint(equalTo: topAnchor).isActive = true
view.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
view.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
view.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
}
ソースコメントで示した疑問のとおりですね。
この理由を確認するために、nibのローディングプロセスを細かく見てみましょうか。
https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/LoadingResources/CocoaNibs/CocoaNibs.html#//apple_ref/doc/uid/10000051i-CH4-SW24 によると、nibのローディングプロセスは以下のような感じです(iOSの場合。Cocoaだけに関わる部分は削ってます)。
- nibファイルのコンテンツ(nibのオブジェクトグラフ)をメモリへとロードする。ただしこの段階ではunarchiveはしない。
- nibのオブジェクトグラフをunarchiveし、インスタンス化する。オブジェクトは
initWithCoder:
メッセージを受けとる。 - コネクションを再構築する。再構築対象には、placeholder objects(iOSの文脈では
File's Owner
のこと)とのコネクションを含む。- Outletコネクションは、
setValue:forKey:
で接続される。KVO通知が走る。 - Actionコネクションは、
addTarget:action:forControlEvents:
で接続される。もしtargetがnilのとき、actionはresponder chainによって扱われる。
- Outletコネクションは、
-
awakeFromNib
が呼ばれる。呼ばれるのはnib-loading codeでインスタンス化されたものに対してだけで、File's Owner
やFirst Responder
のものは呼ばれない。
1.
でオブジェクトグラフがロードされた時点では、それは不完全な存在です。なぜなら、 File's Owner
とのコネクションもあるのに、その File's Owner
がまだ外部から与えられていないからです。
File's Owner
として HogeView
を扱う場合、そのコネクションが構築されるタイミングは、 3.
です。
つまり、 HogeView
は 3.
より前のタイミングでインスタンス化されてないといけないし、 HogeView
と 3.
の結果得られるViewインスタンスは別物にならざるを得ない。
だから、 addSubview
が必要なのです。
nib.instantiate
の引数 options
ってnil以外に何を渡せるの?
ディクショナリのキーとしては UINibExternalObjects
が唯一用意されています。
これを何に使うのかですが……
https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/LoadingResources/CocoaNibs/CocoaNibs.html#//apple_ref/doc/uid/10000051i-CH4-SW24
には、次のようなコード例が載っています。
- (void)applicationDidFinishLaunching:(UIApplication *)application
{
NSArray* topLevelObjs = nil;
NSDictionary* proxies = [NSDictionary dictionaryWithObject:self forKey:@"AppDelegate"];
NSDictionary* options = [NSDictionary dictionaryWithObject:proxies forKey:UINibExternalObjects];
topLevelObjs = [[NSBundle mainBundle] loadNibNamed:@"Main" owner:self options:options];
if ([topLevelObjs count] == 0)
{
NSLog(@"Warning! Could not load myNib file.\n");
return;
}
// Show window
[window makeKeyAndVisible];
}
ですがこれはもはや使われない使用例ですし、ほかに効果的な使いどころがあるのかというと、自分には思いつきません。
誰かエレガントに UINibExternalObjects
を使いこなしている御仁はいらっしゃるのでしょうか。いたら是非、使い方教えてください。
と逆に問いかけつつ、本稿を締めます。