Storyboard よりも Xib を使いたい理由
これは Aizu Advent Calendar 2018 の 7日目の記事です。
6 日目は id:acomagu さんで、8 日目は id:NoahOrberg さんです。
はじめに
Interface Builder (以下「IB」という。) で UIViewController
のレイアウトを組むには、 Storyboard と Xib の2つのファイルを使う方法があります。
その中でも、私が Xib を使う理由について、この記事で紹介します。
Storyboard と Xib
まずは、 Storyboard と Xib について軽く説明します。
Apple のドキュメントによると、Storyboard は、画面間の関係と、画面の中身を表示する視覚的表現だとあります。 developer.apple.com
また、Xib (Nib) は、View や Window 等のアプリ内の視覚的パーツをデザインするためのドキュメントだとあります。 developer.apple.com
IB で視覚的にレイアウトが組めることには、違いはありません。しかし、 UIViewController
の生成方法に違いがあります。
UIViewController
の生成方法の違い
Storyboard の場合では、UIStoryboard
のメソッド instantiateViewController(withIdentifier:)
等で、Storyboard ファイル内のレイアウトを読み込んで UIViewController
を生成します。
Xib の場合では、UIViewController
のイニシャライザ init(nibName:bundle:)
を使い、Nib ファイル内のレイアウトを読み込んで UIViewController
を生成します。
class SourceViewController: UIViewController { // ボタンを押して、次の画面へ遷移させたい時の処理 @IBAction func buttonDidTap(_ sender: UIButton) { // Storyboard の場合 let storyboard = UIStoryboard(name: "Main", bundle: nil) guard let destinationViewController1 = storyboard.instantiateViewController(withIdentifier: "DestinationViewController") as? DestinationViewController else { return } present(destinationViewController1, animated: true) // モーダル遷移 // Xib の場合 let destinationViewController2 = DestinationViewController(nibName: "DestinationViewController", bundle: nil) present(destinationViewController2, animated: true) // モーダル遷移 } }
Storyboard を用いた UIViewController
の生成では、 instantiateViewController(withIdentifier:)
の内部で、UIViewController
の init(coder:)
を呼び、Storyboard をシリアライズして、そのインスタンスを返しています。これは、イニシャライザが init(coder:)
で固定されてしまうため、 以下のような問題が生まれます。
Storyboard の問題
UIViewController
のサブクラスが、新しくストアドプロパティを追加し、クラス外からその値を与えたい場合があるとします。Storyboard の場合では、イニシャライザが init(coder:)
を呼ぶようになっているため、ストアドプロパティを満たすイニシャライザを定義することは出来ず、 init(coder:)
内でそのプロパティを満たす必要があります。 init(coder:)
内で処理を書かなければいけないため、クラス外から値を与えることが出来ません。
class DestinationViewController: UIViewController { let newValue: Int // 新しいストアドプロパティ // Storyboard の場合、このイニシャライザが呼ばれる required init?(coder aDecoder: NSCoder) { newValue = 10 // このイニシャライザ内 (クラス内) でプロパティを満たさなければいけない super.init(coder: aDecoder) } }
新しく追加したプロパティに対してのイニシャライザを利用できないとなると、イニシャライザ時にそのプロパティが満たされることが、言語仕様的に保証が出来ません。そのため、そのプロパティを定義するには、 Optional
または Implicitly Unwrapped Optional
型にする必要があります。それに伴い、そのプロパティを参照する時には、nil チェックが必要になるケースが現れます。
class SourceViewController: UIViewController { @IBAction func buttonDidTap(_ sender: UIButton) { // Storyboard の場合 let storyboard = UIStoryboard(name: "Main", bundle: nil) guard let destinationViewController = storyboard.instantiateViewController(withIdentifier: "DestinationViewController") as? DestinationViewController else { return } // DestinationViewController 外から値を代入 destinationViewController.newValue = 10 present(destinationViewController, animated: true) } } class DestinationViewController: UIViewController { // Optional または Implicitly Unwrapped Optional の必要がある var newValue: Int? func doSomething() { if let newValue = newValue { // nil チェック // newValue を用いた処理 } } }
nil チェックを回避するには、デフォルト値を与え、Optional
または Implicitly Unwrapped Optional
で無くす必要があります。
また、別の問題として、デフォルト値の有無に関係なく、本来はクラス外へ公開したくない場合でも、そのプロパティをミュータブル (var
) にして公開しなければいけません。
class SourceViewController: UIViewController { @IBAction func buttonDidTap(_ sender: UIButton) { // Storyboard の場合 let storyboard = UIStoryboard(name: "Main", bundle: nil) guard let destinationViewController = storyboard.instantiateViewController(withIdentifier: "DestinationViewController") as? DestinationViewController else { return } // DestinationViewController 外から値を代入 destinationViewController.newValue = 10 present(destinationViewController, animated: true) } } class DestinationViewController: UIViewController { // デフォルト値を代入 // クラス外から代入するために、 非公開 (private) に出来ない var newValue: Int = 0 }
まとめると、 新しくストアドプロパティを追加した UIViewController
のサブクラスを Storyboard から生成し、そのプロパティをクラス外から与えたい場合には、以下の問題があります。
- そのプロパティに対するイニシャライザを定義できないため、そのプロパティを
Optional
またはImplicitly Unwrapped Optional
型にする必要があり、 nil チェックが必要になるケースが現れる - nil チェックを回避することもできるが、適切なデフォルト値を代入する必要がある
- クラス外から、そのプロパティを代入するために、 必ず ミュータブル (
var
)、かつ、公開するプロパティでなければならない
Storyboard の問題の解決
Xib の場合、 UIViewController
の init(nibName:bundle:)
を呼ぶことが出来れば、Xib のレイアウトを読み込んだインスタンスを生成出来ます。そのため、UIViewController
のサブクラスに、新しくストアドプロパティを追加したとしても、init(nibName:bundle:)
を内部で呼ぶようなイニシャライザを定義して呼べば、Xib のレイアウトを読み込み、かつ、そのプロパティを満たすことが出来ます。
class SourceViewController: UIViewController { @IBAction func buttonDidTap(_ sender: UIButton) { // Xib の場合 let destinationViewController = DestinationViewController(value: 10, nibName: "DestinationViewController", bundle: nil) present(destinationViewController, animated: true) } } class DestinationViewController: UIViewController { // イミュータブルや非公開に出来る private let newValue: Int required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } init(value: Int, nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { self.newValue = value super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) } func doSomething() { // newValue を用いた処理 (nilチェックの必要なし) } }
このイニシャライザは、単に以下のように、プロトコルで表現することも出来ます。
protocol NibInstantiableViewController where Self: UIViewController { associatedtype Input init(input: Input, nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) } class DestinationViewController: UIViewController, NibInstantiableViewController { typealias Input = Int // Input には任意の型を与えられる private let newValue: Input required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } required init(input: Input, nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { self.newValue = input // input を用いて、ストアドプロパティを満たす super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) } }
Storyboard から生成する場合では、クラス外から、新しいストアドプロパティを代入する場合は、そのプロパティを必ずミュータブル (var
) 、かつ、公開する必要がありました。
しかし、 Xib の場合では、自前でイニシャライザを利用することが出来るため、必要に応じて、そのプロパティをイミュータブル (let
) や 非公開 (private
) にすることが出来るようになります。
まとめ
上記で触れていませんでしたが 、Storyboard の場合、Storyboard ID の指定や、生成した UIViewController
のダウンキャストなどの手間もあります。Xib の場合、 直接そのクラスのイニシャライザから生成するため、ダウンキャストする必要もなく、Storyboard が抱えるイニシャライズの問題も解消できます。
もちろん、 UIViewController
間の関係を組むことが出来る等、 Storyboard にだけが持つ長所もあります。しかし、私は、それよりも Xib が持つ恩恵の方が大きいと考えているため、 Xib を利用するようにしています。