読者です 読者をやめる 読者になる 読者になる

iOS 開発で storyboard と xib をうまく使い分けるプラクティス

Web エンジニアだったはずがひょんなことから iOS アプリを書き始めてはや3ヶ月。ヘルスケア事業部の濱田です。

iOS アプリで画面遷移を実現するためには様々な方法があります。

  • コードのみを使う方法
  • xib を使う方法
  • storyboardを使う方法
  • etc.

初めはかなり混乱しましたが、最終的には storyboard と xib の合わせ技に落ち着きました。 今回はこの方法についてご紹介します。

storyboard を使うか、xib を使うか、それが問題だ

アプリの UI 部品の配置は結構たいへんな作業です。とくに Autolayout の制約の設定などは、コードのみで設定するのは困難でしょう。Interface Builder の支援をなるべく活用したいところです。 そこで、storyboard もしくは xib ファイルを利用して ViewController(以下 VC) を 書いていくわけですが、どちらにも利点と欠点があり、場合によって使い分ける必要があります。

よくある定義の仕方としては、以下の2つがあるようです。

storyboard に複数の VC の定義と遷移を一緒に書く

Pros

  • アプリの遷移を集約して書くことができ、処理の流れが理解しやすい

Cons

  • storyboard がどんどん大きくなって編集がコンフリクトする
  • 同じ VC を複数個置きたいと思っても、ビューの定義を再利用できない

xib ファイルごとに VC を定義して、遷移の定義はコードで行う

Pros

  • 画面を再利用できる

Cons

  • コードでつないでいると、VCのつながりが把握しにくい

前者の方法だとコードは少なくなりますが、VC の再利用を行うことはできません。 後者の方法を使うと、VC を複数の箇所で使いまわせるのですが、画面の流れを把握するのは難しくなります。

そこでちょっとだけ工夫をして、画面の流れは storyboard で定義しつつ、VC のビュー定義は xib ファイルにおいておく折衷方式にしました。

xib と storyboard を組み合わせて使う

折衷方式には、以下のようなメリット/デメリットがあります。

Pros
  • VC のクラス定義、ビュー定義がそれぞれ1ファイルに対応するので取り回しやすい
  • VCを複数のstoryboardで再利用できる(DRY!)
Cons
  • storyboard の画面上では UI パーツの配置が確認できない

今回取り組んだアプリでは同じ VC が異なる storyboard の遷移の中に現れることがしばしばあり、上記の折衷方式で効率よく実装できました。

作業手順

例として、FirstViewController, SecondViewController という2つの画面を持つアプリを考えます。

FirstViewController あるボタンを押すと、SecondViewController がモーダル表示されるという簡単なアプリです。

f:id:manemone:20150623204145g:plain

xib ファイルにビューのみを定義する

まず、FirstViewController、SecondViewController それぞれのビュー定義を行う xib ファイルを作成します。 xib の中には、VC で表示するための UIView を一つおき、その中に画面部品を配置していきます。(VC ではなく、View のみを定義するのがミソ)。

File's owner で xib と VC を関連付ける

次に、xib の File's owner に SecondViewController クラスを設定します。これによって、xib と SecondViewController の紐付けが行われ、ビュー要素へのアウトレットを持つことができるようになります。

f:id:manemone:20150623203041p:plain

f:id:manemone:20150623203043p:plain

VC の loadView() をオーバライド

さらに、VC の loadView() を以下のようにオーバーライドします。

// Yes, we're on Swift :-)
class SecondViewController: UIViewController {
  ...
    override func loadView() {
      if let view = UINib(nibName: "SecondView", bundle: nil).instantiateWithOwner(self, options: nil).first as? UIView {
        self.view = view
      }
    }
  ...
}

nibname: にはさきほどの xib ファイルの名前を書きます。 loadView() はその名の通り、VC が管理するビューを読み込むためのメソッドです。通常の動作では VC が格納された storyboard や xib ファイルからビューを読み込むようになっています。ここでは、必ず先ほどの xib のビューを読み込むように設定しておきます。

storyboard でフローを定義する

ストーリーボードでビューの流れを定義してみましょう。FirstViewController のボタンを押すと SecondViewController がモーダルとして表示されるようにします。

VC を一つ追加して、中の View を削除します。 Indentity Inspector の Custom Class で、先ほど作ったクラスを指定し、scene の中に指定したクラス名が表示されれば OK です。この状態でシミュレータで走らせてみると、xib ファイルに記述したビューが読み込まれていることがわかるはずです。

f:id:manemone:20150623203045p:plain

f:id:manemone:20150623203047p:plain

segue で接続

あとは、通常の storyboard 作成と同じように、追加した VC をマニュアル segue で接続し、適切な identifier を付けておきます。ここではそれぞれ "buttonA"、"buttonB" としました。 VC の方では、遷移を促すアクションが発生したとき、設定した identifier で segue を実行すれば OK です。

f:id:manemone:20150623203044p:plain

プロパティを IB 経由で与えて調整する

画面の背景色等、UIに与える初期値を storyboard 側から調整したいときは、Interface Builder の User Defined Runtime Attributes を活用するとよいです。例えば、2つめの SecondViewController に表示されるラベルの色を変更したいときは以下のようにします。

SecondViewController で以下のように設定しておきます

class SecondViewController: UIViewController {
  ...
    @IBOutlet weak var label: UILabel!

    // この変数に IB から色をセット
    var labelBackgroundColor = UIColor(red: 240.0/255, green: 118.0/255, blue: 101.0/255, alpha: 1.0)

    override func viewDidLoad() {
      super.viewDidLoad()

        // 変数 labelBackgroundColor に設定された色をラベルの背景色にする
        self.label.backgroundColor = labelBackgroundColor
    }
  ...
}

Interface Builder の storyboard で SecondViewController を選択し、User Defined Runtime Attributes に色を設定します。

f:id:manemone:20150623204227p:plain

以上で、冒頭の GIF と同じ挙動の遷移が実現できました。

載せきれなかったコードは以下のリポジトリに置いてあります: ReusableVCDemo

いかがでしょうか。関わった仕事では、基本的に上述のやりかたで VC のビューと画面遷移を記述し、storyboard をまたぐ遷移だけコードで記述するようにしています。 画面遷移を細かい storyboard に分割して定義する場合、同じ VC が複数の storyboard に現れるのはよくあることです。そんなときでも VC を DRY に記述しつつ、かつ storyboard の良さであるビジュアルベースの遷移定義ができて便利ですので、よろしければ参考にしてみてください。

/* */ @import "/css/theme/report/report.css"; /* */ /* */ body{ background-image: url('http://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527163350.png'); background-repeat: repeat-x; background-color:transparent; background-attachment: scroll; background-position: left top;} /* */ body{ border-top: 3px solid orange; color: #3c3c3c; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; line-height: 1.8; font-size: 16px; } a { text-decoration: underline; color: #693e1c; } a:hover { color: #80400e; text-decoration: underline; } .entry-title a{ color: rgb(176, 108, 28); cursor: auto; display: inline; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; font-size: 30px; font-weight: bold; height: auto; line-height: 40.5px; text-decoration: underline solid rgb(176, 108, 28); width: auto; line-height: 1.35; } .date a { color: #9b8b6c; font-size: 14px; text-decoration: none; font-weight: normal; } .urllist-title-link { font-size: 14px; } /* Recent Entries */ .recent-entries a{ color: #693e1c; } .recent-entries a:visited { color: #4d2200; text-decoration: none; } .hatena-module-recent-entries li { padding-bottom: 8px; border-bottom-width: 0px; } /*Widget*/ .hatena-module-body li { list-style-type: circle; } .hatena-module-body a{ text-decoration: none; } .hatena-module-body a:hover{ text-decoration: underline; } /* Widget name */ .hatena-module-title, .hatena-module-title a{ color: #b06c1c; margin-top: 20px; margin-bottom: 7px; } /* work frame*/ #container { width: 970px; text-align: center; margin: 0 auto; background: transparent; padding: 0 30px; } #wrapper { float: left; overflow: hidden; width: 660px; } #box2 { width: 240px; float: right; font-size: 14px; word-wrap: break-word; } /*#blog-title-inner{*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-position: left 0px;*/ /*}*/ /*.header-image-only #blog-title-inner {*/ /*background-repeat: no-repeat;*/ /*position: relative;*/ /*height: 200px;*/ /*display: none;*/ /*}*/ /*#blog-title {*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-image: url('http://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527172848.png');*/ /*background-repeat: no-repeat;*/ /*background-position: left 0px;*/ /*}*/