Interface builderでカスタムViewのライブレンダリング

  • 87
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

【第9回】potatotips@Fablicで話した内容です。
Interface builder(IB)について話しました。

IBはXcode6からカスタムViewのライブレンダリングに対応しました

Xcode5の時代、iOSエンジニアは長方形の白いカスタムViewを見て、脳内レンダリングを行う必要がありました。

Xcode6からは、カスタムViewのレンダリング結果をIBの中で確認することができます。

Screen Shot 2014-09-25 at 21.02.28.png

サンプルとして、どこにでもありそうなボタンを実装してみました。
標準ではないボタンのデザインをIBの中で確認できます。

サンプルプロジェクト

Xcode6CocoaPodsが入っている方は、以下のコマンドでこのプロジェクトを動かすことができます。

$ pod try HRButton

Main.storyboardを見るとライブレンダリングに対応していることが確認できます。

ライブレンダリングに対応する

使うマクロは、IB_DESIGNABLEIBInspectableのふたつです。

IB_DESIGNABLEはクラスの宣言の前に記述して、IBにレンダリングに対応しているカスタムViewであることを伝えます。

IBInspectableはPropertyの定義の中、ちょうどIBOutletと同じ位置に記述します。
UIKit標準のView(UIButtonなど)と同じようにカスタムViewのPropertyもIB上で設定することができます。

#import <UIKit/UIKit.h>

IB_DESIGNABLE
@interface HRButton : UIControl

@property (nonatomic, strong) IBInspectable UIColor *highlightedTintColor;
@property (nonatomic, strong) IBInspectable NSString *title;
@property (nonatomic, strong) IBInspectable UIColor *titleColor;
@property (nonatomic) IBInspectable NSUInteger cornerRadius;

@end

IBInspectableを使うと、以下のようにインスペクタ経由で設定できるようになります。

Screen Shot 2014-09-25 at 21.21.50.png

インスペクタを操作すると、リアルタイムで更新されることが確認できると思います。

強力な機能です

1px単位の調整をどれだけ繰り返すことができるかというのはとても重要で、アプリの品質に効いてくる要素だと思っています。

この繰り返しのサイクルが、「修正 > ビルド > 確認」から「修正 > 確認」になるということは、大きなインパクトがあります。

Screen Shot 2014-09-25 at 21.37.29.png

Debug

普通の環境とは異なる状態で実行されるのでDebugの方法が異なります。

  • カスタムViewのメソッドの中にBreakPointを仕込む
  • IBを開いて対象のカスタムViewを選択
  • Editor > Debug Selected Viewを実行

これで、lldbを使ったDebugを行うことができます。

実行環境

さて、lldbを使えるようになったところで、どんな環境でカスタムViewが実行され表示されているのかを調べてみます。

(lldb) po [[UIDevice currentDevice] model]
iPhone Simulator

端末をiPhone Simulatorと認識しているようです。


(lldb) po self.superview
<UIWindow: 0x7fef03052cb0; frame = (0 0; 277 46); hidden = YES; autoresize = W+H;...
`
superviewがUIWindowでした。UIWindowのサイズはカスタムViewと同じです。

BreakPointをdrawRect:の中に置いてbacktraceを見てます。

(lldb) thread backtrace all
  * frame #0: 0x000000021e7db3cc HRButton`-[HRButton drawRect:](self=0x00007fef0054e2c0, _cmd=0x0000000114bef273, rect=CGRect at 0x00007fff5063bbb0) + 28 at HRButton.m:73
     -- 略 --
    frame #6: 0x000000011152b040 QuartzCore`-[CALayer renderInContext:] + 151
    frame #7: 0x000000010f5d53e1 IBDesignablesAgentCocoaTouch`-[IBCoreGraphicsViewRenderer createBitmapInfo] + 1141
    frame #8: 0x000000010f5c49a4 IBDesignablesAgentCocoaTouch`IBImageDataForViewWithScaleFactor + 136
     -- 略 --
    frame #29: 0x000000010f5fafe0 IBDesignablesAgentCocoaTouch`main + 34
    frame #30: 0x000000011647e145 libdyld.dylib`start + 1
  • アプリ自体をビルドしているわけではない
  • Bitmapとして書き出した結果をIBに表示している

というようなことが分かります。

この実装から、今のところ次のような制限があります。

  • AppDelegateは呼ばれないので、UIAppearanceで定義したスタイルはIB上では反映されません
  • 書き出した画像をIBで表示しているだけで、生きたViewがIB上に配置されるわけではない
    • 非同期でaddSubViewしたり、アニメーション付きで表示されるViewはそのままでは表示されない

TARGET_INTERFACE_BUILDERprepareForInterfaceBuilderを使うと表示されるようになります。ドキュメントを参考にしてください。

個人的にはライブレンダリングは効率化よく実装するためのものなので、既存のカスタムViewを完璧に対応させる必要はないと思っています。

話さなかったこと

検証できなかった、微妙だったなどの理由で話さなかったこと。

ライフサイクル

以下のようなライフサイクルでカスタムViewがレンダリングされている(と認識しています)

  • カスタムVIewに変更があると、IBDesignablesAgentCocoaTouchに組み込まれてビルドされる
  • iPhone Simulatorのような環境で実行される(開発者には見えない)
  • initWithFrame:で初期化
  • 小さなUIWindowにaddSubview
  • レンダリング結果を画像化
  • IB上に画像を表示

initWithFrameに対応する必要あり

IBで表示するのにinitWithCoderではなく、initWithFrameです。
実行時にはinitWithCoderが呼ばれるのでどちらで呼ばれても動作するように実装しなければいけません。

カテゴリでも使える

例えば、すべてのUIViewにdebugオプションをつけるというようなことができます。
任意のViewにIB上で枠線をつけたりとか、そんな感じのことができるようになるのですが、非同期処理が難しかったり、カテゴリでaddSubviewのイベントを取得するのが難しかったりで、あまり役にたつような物を作れませんでした。

まとめ

ライブレンダリングを使おう!