今日のスライドです https://t.co/xIkI9IzEFZ #potatotips
— yomoapp(本人) (@yomoapp) September 24, 2014
【第9回】potatotips@Fablicで話した内容です。
Interface builder(IB)について話しました。
IBはXcode6からカスタムViewのライブレンダリングに対応しました
Xcode5の時代、iOSエンジニアは長方形の白いカスタムViewを見て、脳内レンダリングを行う必要がありました。
Xcode6からは、カスタムViewのレンダリング結果をIBの中で確認することができます。
サンプルとして、どこにでもありそうなボタンを実装してみました。
標準ではないボタンのデザインをIBの中で確認できます。
サンプルプロジェクト
Xcode6と CocoaPodsが入っている方は、以下のコマンドでこのプロジェクトを動かすことができます。
$ pod try HRButton
Main.storyboard
を見るとライブレンダリングに対応していることが確認できます。
ライブレンダリングに対応する
使うマクロは、IB_DESIGNABLE
とIBInspectable
のふたつです。
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
を使うと、以下のようにインスペクタ経由で設定できるようになります。
インスペクタを操作すると、リアルタイムで更新されることが確認できると思います。
強力な機能です
1px単位の調整をどれだけ繰り返すことができるかというのはとても重要で、アプリの品質に効いてくる要素だと思っています。
この繰り返しのサイクルが、「修正 > ビルド > 確認」から「修正 > 確認」になるということは、大きなインパクトがあります。
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_BUILDER
やprepareForInterfaceBuilder
を使うと表示されるようになります。ドキュメントを参考にしてください。
個人的にはライブレンダリングは効率化よく実装するためのものなので、既存のカスタムViewを完璧に対応させる必要はないと思っています。
話さなかったこと
検証できなかった、微妙だったなどの理由で話さなかったこと。
ライフサイクル
以下のようなライフサイクルでカスタムViewがレンダリングされている(と認識しています)
- カスタムVIewに変更があると、
IBDesignablesAgentCocoaTouch
に組み込まれてビルドされる - iPhone Simulatorのような環境で実行される(開発者には見えない)
-
initWithFrame:
で初期化 - 小さなUIWindowにaddSubview
- レンダリング結果を画像化
- IB上に画像を表示
initWithFrameに対応する必要あり
IBで表示するのにinitWithCoder
ではなく、initWithFrame
です。
実行時にはinitWithCoder
が呼ばれるのでどちらで呼ばれても動作するように実装しなければいけません。
カテゴリでも使える
例えば、すべてのUIViewにdebugオプションをつけるというようなことができます。
任意のViewにIB上で枠線をつけたりとか、そんな感じのことができるようになるのですが、非同期処理が難しかったり、カテゴリでaddSubviewのイベントを取得するのが難しかったりで、あまり役にたつような物を作れませんでした。
まとめ
ライブレンダリングを使おう!