今日のスライドです 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 IBDesignablesAgentCocoaTouchmain + 34 frame #30: 0x000000011647e145 libdyld.dylib
start + 1
- アプリ自体をビルドしているわけではない
- Bitmapとして書き出した結果をIBに表示している
というようなことが分かります。
この実装から、今のところ次のような制限があります。
- `AppDelegate`は呼ばれないので、`UIAppearance`で定義したスタイルはIB上では反映されません
- 書き出した画像をIBで表示しているだけで、生きたViewがIB上に配置されるわけではない
- 非同期で`addSubView`したり、アニメーション付きで表示されるViewはそのままでは表示されない
`TARGET_INTERFACE_BUILDER`や`prepareForInterfaceBuilder`を使うと表示されるようになります。[ドキュメント](https://developer.apple.com/library/ios/recipes/xcode_help-IB_objects_media/chapters/CreatingaLiveViewofaCustomObject.html#//apple_ref/doc/uid/TP40014224-CH41-SW1)を参考にしてください。
個人的にはライブレンダリングは効率化よく実装するためのものなので、既存のカスタムViewを完璧に対応させる必要はないと思っています。
# 話さなかったこと
検証できなかった、微妙だったなどの理由で話さなかったこと。
## ライフサイクル
以下のようなライフサイクルでカスタムViewがレンダリングされている(と認識しています)
- カスタムVIewに変更があると、`IBDesignablesAgentCocoaTouch`に組み込まれてビルドされる
- iPhone Simulatorのような環境で実行される(開発者には見えない)
- `initWithFrame:`で初期化
- 小さなUIWindowにaddSubview
- レンダリング結果を画像化
- IB上に画像を表示
## initWithFrameに対応する必要あり
IBで表示するのに`initWithCoder`ではなく、`initWithFrame`です。
実行時には`initWithCoder`が呼ばれるのでどちらで呼ばれても動作するように実装しなければいけません。
## カテゴリでも使える
例えば、すべてのUIViewにdebugオプションをつけるというようなことができます。
任意のViewにIB上で枠線をつけたりとか、そんな感じのことができるようになるのですが、非同期処理が難しかったり、カテゴリでaddSubviewのイベントを取得するのが難しかったりで、あまり役にたつような物を作れませんでした。
# まとめ
ライブレンダリングを使おう!