hiragram no blog

iOSとか

xibからビューのインスタンスを生成するコードの治安を良くする

AwesomeView.xibからAwesomeViewインスタンスを作りたい時、普通にUINibのイニシャライザを使って呼び出すと

let awesome = UINib(nibName: "AwesomeView", bundle: nil).instantiateWithOwner(nil, options: nil).first as! AwesomeView

こんなに治安が悪い。

  • nibNameを文字リテラルで指定している
  • force cast (as!)

この辺が治安を悪くしている。これをプロジェクト内のあちこちに撒き散らすのは本当にかなしくてみじめなことになるので、こんな方法を考えた。

まず「xibからインスタンス生成できる」という性質を示すプロトコルを作る。

protocol NibInstantiatable {
  // 上で言う、"AwesomeView"が入ってるプロパティ
  static var nibName: String { get }

  // そとから呼び出すメソッド
  static func instantiateFromNibWithOwner(ownerOrNil: AnyObject?, options: [NSObject: AnyObject]?) -> Self
}

次に、UIView用にデフォルト実装を持たせる。

extension NibInstantiatable where Self: UIView {
  static var nibName: String {
    get {
      // 上で言う"AwesomeView"みたいな、「自分のクラス名の文字列」を返す
      return String(Self.self)
    }
  }

  static func instantiateFromNibWithOwner(ownerOrNil: AnyObject?, options: [NSObject: AnyObject]?) -> Self {
    // 治安の悪いコードはここだけに閉じ込められる。
    return UINib(nibName: nibName, bundle: nil).instantiateWithOwner(ownerOrNil, options: options).first as! Self
  }
}

AwesomeView.swiftの変更は

- class AwesomeView: UIView {
+ final class AwesomeView: UIView, NibInstantiatable {

}

と、NibInstantiatableの指定をするだけ。protocolのメソッドでSelfを使っている都合からfinalの指定が必要になるが、ビュークラスの継承はしない、というルールが自分の中にあるので問題なし。

UIViewのサブクラスなのでこれでprotocol extensionに記載されたコードが実行される。 呼び出し側はこんな感じ。

let awesome = AwesomeView.instantiateFromNibWithOwner(nil, options: nil)

AwesomeViewのインスタンスを作っていることが一発でわかるし、

let awesome = UINib(nibName: "AwesomeView", bundle: nil).instantiateWithOwner(nil, options: nil).first as! AwesomeView

これと比べると随分治安がよくなったのではないだろうか。

このprotocol extensionの実装は、xibのファイル名が「クラス名.xib」の時だけ有効である。もし何らかの理由でクラス名とxib名が違うのであれば(リネームしろよ感はあるものの)、以下のように

final class AwesomeView: UIView, NibInstantiatable {
  static var nibName = "SuperAwesomeView"
}

nibNameだけ自分のクラスに書いてやれば、protocol extensionではなくこっちが優先されるのでこれも対応が楽。