Storyboard よりも Xib を使いたい理由

これは Aizu Advent Calendar 2018 の 7日目の記事です。
6 日目は id:acomagu さんで、8 日目は id:NoahOrberg さんです。

adventar.org

はじめに

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:) の内部で、UIViewControllerinit(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 の場合、 UIViewControllerinit(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 を利用するようにしています。