Think Big Act Local

iPhone開発を軸にブレブレの記事を書いていきます。

カスタムViewをNibから初期化し、IBDesignableとIBInspectableで便利に使う

iOS開発をしていると、独自のViewを作りたい場合があります。
実現する方法は色々あると思うのですが、最近自分がやっている方法をまとめます。

(以下で実装しているコードは GitHub にすべてあげています)

目指すゴール

  • Storyboard / Interface Buidler から初期化できる
  • コードからも初期化できる
  • IBDesignable & IBInspectable に対応している
  • 各画面から使いやすい(汎用性が高い)

上記を満たすカスタムViewを目指します。

f:id:himaratsu20150725222016p:plain

こんな感じの、ユーザーにレビューをお願いするViewで、
↓みたいにStoryboard上から値をセットできるものを作ってみます。

http://cdn-ak.f.st-hatena.com/images/fotolife/h/himaratsu/20150726/20150726001213.gif

実装の流れ

1. カスタムViewのxibとクラスを用意する

(1) xibを作成

MyCustomView.xib というファイルを作成します。
Size を "Freeform" にして適当にリサイズし、Status Bar を "None" にします。

xibをつくってリサイズする

(2) コンポーネントの配置とレイアウト

上から UILabel, UIImageView, UIButton を追加して AutoLayout をセットします。

f:id:himaratsu20150725222016p:plain

(3) クラスの作成

MyCustomView クラスを UIView のサブクラスとして作成します。

class MyCustomView: UIView { ... }

2. xibとクラスを紐づける

xib を 開き、File's Owner にカスタムViewのクラス名を入力します。

xibとクラスを紐づける

View とではなく、File's Owner と紐づけます。

3. 初期化のコードを書く

以下のように初期化の実装を記述します。

class MyCustomView: UIView {
    // コードから初期化はここから
    override init(frame: CGRect) {
        super.init(frame: frame)
        comminInit()
    }
    
    // Storyboard/xib から初期化はここから
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        comminInit()
    }

    // xibからカスタムViewを読み込んで準備する
    private func comminInit() {
        // MyCustomView.xib からカスタムViewをロードする
        let bundle = NSBundle(forClass: self.dynamicType)
        let nib = UINib(nibName: "MyCustomView", bundle: bundle)
        let view = nib.instantiateWithOwner(self, options: nil).first as! UIView
        addSubview(view)
        
        // カスタムViewのサイズを自分自身と同じサイズにする
        view.translatesAutoresizingMaskIntoConstraints = false
        let bindings = ["view": view]
        addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[view]|",
            options:NSLayoutFormatOptions(rawValue: 0),
            metrics:nil,
            views: bindings))
        addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[view]|",
            options:NSLayoutFormatOptions(rawValue: 0),
            metrics:nil,
            views: bindings))
    }

init(frame: CGRect) はコードから初期化する場合に呼ばれ、
init(coder aDecoder: NSCoder) は Storyboard/xib からの初期化で呼ばれます。

ここでは初期化の処理を commonInit() メソッドに共通化してどちらからも呼んでいるので、コードからでも Storyboard/xib からでも初期化して使うことができます。

4. IBOutlet と IBAction を設定する

xibからCtrl+ドラッグで、IBOutlet や IBAction を設定します。

@IBOutlet weak private var titleLabel: UILabel!
@IBOutlet weak private var iconImageView: UIImageView!
@IBOutlet weak private var okButton: UIButton!

// 「ALPACAを応援する」ボタンが押された際の挙動
@IBAction func okButtonTouched(sender: AnyObject) {
    let appStoreUrl = "https://itunes.apple.com/app/id934444072?mt=8"
    if let URL = NSURL(string: appStoreUrl) {
        if UIApplication.sharedApplication().canOpenURL(URL) {
            UIApplication.sharedApplication().openURL(URL)
        }
    }
}

ここまでで動作としては完成です。

5. @IBDesignable を指定する

すでに動くものはできていますが、さらに使いやすく工夫していきます。

Xcode 6以降では、@IBDesignableを対象クラスに指定することで、Storyboard 上でカスタムViewの見た目を確認することができます。

@IBDesignable
class MyCustomView: UIView {
...

上記のように記述すると、Storyboard上では以下の右のように表示されます。

IBDesignable 有りと無し

便利ですね!

6. @IBInspectable を設定する

さらに便利に使うために、@IBInspectable を設定します。

これも @IBDesignable と同じく Xcode 6 から使えるようになったもので、設定するとカスタムクラスのプロパティの値をStoryboard上からセットできます。

IBInspectableを指定するとここから値をセットできる

上記のようにするためには、MyCustomView.swift に以下のように記述します。

@IBDesignable
class MyCustomView: UIView {
    // viewの枠線の色
    @IBInspectable var borderColor: UIColor = UIColor.clearColor() {
        didSet {
            self.layer.borderColor = borderColor.CGColor
        }
    }

    // viewの枠線の太さ
    @IBInspectable var borderWidth: CGFloat = 0 {
        didSet {
            self.layer.borderWidth = borderWidth
        }
    }
    
    // viewの角丸
    @IBInspectable var cornerRadius: CGFloat = 0 {
        didSet {
            self.layer.cornerRadius = cornerRadius
            self.layer.masksToBounds = true
        }
    }
    
    // 「レビューのおねがい」部分のテキスト
    @IBInspectable var titleText: String = "" {
        didSet {
            titleLabel.text = titleText
        }
    }
    
    // アルパカが表示されている画像
    @IBInspectable var iconImage: UIImage? {
        didSet {
            iconImageView.image = iconImage
        }
    }

    // 「ALPACAを応援する」ボタンのテキスト
    @IBInspectable var buttonTitle: String = "" {
        didSet {
            okButton.setTitle(buttonTitle, forState: .Normal)
        }
    }

カスタムクラスのプロパティは、実はこれまでも User Defined Runtime Attributes を使えば値のセットは可能でした。

User Defined Runtime Attiributes よりも IBInspectable を使おう
User Defined Runtime Attributes

しかし、

  • 不正な値をセットしてしまうとクラッシュしてしまう
  • ここの存在を知らない開発者がいると動作を理解しづらい

など使いづらい部分があったため、今後は IBInspectable を使うのが良いでしょう。

使い方

以上でカスタムView側の実装は完成です。
最後にこのカスタムViewの使い方です。

Storyboard/xibから初期化して使う場合

UIView を Storyboard に貼り付け、クラス名に MyCustomView を指定します。

f:id:himaratsu:20150726001117g:plain

Storyboard上の表示を確認しながら値を変更します。

f:id:himaratsu:20150726001213g:plain

コードから初期化して使う場合

frame または AutoLayout の制約でサイズを指定し、addSubview して使います。

let customView = MyCustomView(frame: CGRectMake(50, 200, 280, 200))
view.addSubview(customView)

まとめ

カスタムViewをNibから初期化し、IBDesignable と IBInspectable を利用して便利に使えるようにする手順でした。
より良い方法、より簡単な方法があれば教えていただけますと幸いです。

サンプルコード

今回使ったコードはこちらに置いています。

github.com

参考資料

qiita.com

オススメ書籍

初期設定から CocoaPods を使った実践チックな内容までを1冊で学習できます。

AutoLayoutの理解を深めたい方にオススメです。