プロジェクト変わる度に同じ議論をしていて、もうこれはまとめた方が良いなと。。
# 概要 IBDesignable(およびIBInspectable)への対応方法まとめ。基本
IBDesignable、IBInspectableとは
IBファイル(StoryboardやXib)の実装がとてもしやすくなる機能。
(なお、これ以外のメリットはない🌛)
- **登場以前:**カスタムビューをIBファイル(StoryboardやXib)上で使う際「実機上ではきちんと表示されるものの、IBファイル上では真っ白なビュー」という状態だったため、新規開発はおろか追加開発もデバッグもしづらかった。
- **登場後:**カスタムビューでもIBファイル上で実機のようにレンダリングしてくれるようになり、さらにIBInspectableによってカスタムインターフェースを追加できるようになった。
IBDesignableを実装する方法
@IBDesignable final class DesignableView: UIView {
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override init(frame: CGRect) {
super.init(frame: frame)
}
}
やることは2つ。
- クラス宣言の頭に
@IBDesinnable
アノテーションを付加する。 - イニシャライザを実装する。
IBInspectableを実装する方法
extension UITextView {
@IBInspectable var topPadding: Float {
get { return Float(self.textContainerInset.top) }
set { self.textContainerInset.top = CGFloat(newValue) }
}
}
やることは1つ。プロパティ宣言の頭に@IBInspectable
アノテーションを付加するだけ。
ただし、指定できる型は下記いずれかのみ。
- Bool
- Int
- Double
- String
- Float
- CGPoint
- CGSize
- CGRect
- UIColor
- UIImage
実装で気をつけるべきこと
- 標準インターフェースと被るIBInspectableは作らない
- 標準インターフェースと被るIBInspectableを実装すると、後々のデバッグの邪魔になる。
- 仕方なく作る場合は、IBファイルでのみ使うよう警告アノテーションを付加する。
- 基底クラスは作らない
- 基底クラスを作ると後々のデバッグで影響範囲の特定が困難になる。
- なお、チームの方針によるが、個人的には、IBDesignable/IBInspectableの実装を簡単にするためだけに基底クラスを作るのは微妙かと。
- IBファイルでのみ使うクラスやプロパティには警告アノテーションを付加
- IBDesignable/IBInspectableの目的は「IBファイルの実装をアシストする」ことでしかないので、これらに関する実装でSwiftファイル側のコードが散らかるのはデバッグ観点で避けるべき。
- よって、実装する際、IBファイルでIBファイル専用Viewクラス(例: CustomLabel)を使う場合は、SwiftファイルのIBOutletは標準クラス(例: UILabel)にしたりする方が望ましい。
- ただし、チーム内で「実装気をつけてね」というルールを作ってもだいたい忘れたりするため、警告アノテーションを付加してSwiftファイル側でそれらを使った場合にビルド失敗するようにする。
@available(*, unavailable, message: "やっちゃったね。これはIBファイル専用クラスだよ。Swiftファイルで参照する際は継承元クラスで参照してね👴", renamed: "UILabel")
対応例
対応例1: 影や楕円のついたビューを作りたい
// MARK: UIView
@available(*, unavailable, message: "Only use it at Storybord or Xib. When referring it from their file to Swift file, replace reference class name to inherited class.", renamed: "UIView")
@IBDesignable final class DesignableView: UIView {
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override init(frame: CGRect) {
super.init(frame: frame)
}
}
- 利用Viewを継承したIBファイル(StoryboardやXib)専用クラスを作成
- 描画専門となるようアノテーション付加とイニシャライザ実装以外はしない。
- ちなみに、拡張することはないクラスなので、見通しよくなるようUIViewやUILabelなどの各拡張クラスは1ファイルにまとめていいかと。
- Clip to Bounds(「Viewの中身を切り取るか」の指定)を指定
- 影をつける場合は
false
、角丸にする場合はtrue
にする。なお、コードで指定する場合はlayer.masksToBounds
で指定。 - 影付きかつ角丸のUILabelやUIImageViewを作りたい場合は、
Clip to Bounds = false
の角丸&影付きのUIViewを作成し、その上にClip to Bounds = true
で角丸指定した対象ビューを置く。
-
UserDefinedRuntimeAttributesを設定
layer.~
のような形でCALayerプロパティを追加。
No. | 項目 | Key Path | Type | Value例 | 備考 |
---|---|---|---|---|---|
1 | 影の半径 | layer.shadowRadius | Number | 20.0 | |
2 | 影のかかる方向 | layer.shadowOffset | Size | {10.0,10.0} | 横と縦。下目に影を落としたい場合は、{0,5}等で指定。 |
3 | 影の透明度 | layer.shadowOpacity | Number | 0.7 | 0~1の間 |
4 | 角丸の半径 | layer.cornerRadius | Number | 10.0 | これをビューの高さの半分の値にすると楕円形になる |
5 | 枠線の幅 | layer.borderWidth | Number | 1.0 |
対応例2: 影や枠線に色をつけたい
extension CALayer {
@available(*, unavailable, message: "Only use it at Storybord or Xib.", renamed: "borderColor")
@IBInspectable var borderUIColor: UIColor? {
get { return borderColor.map { UIColor(cgColor: $0) } }
set { borderColor = newValue?.cgColor }
}
@available(*, unavailable, message: "Only use it at Storybord or Xib.", renamed: "shadowColor")
@IBInspectable var shadowUIColor: UIColor? {
get { return shadowColor.map { UIColor(cgColor: $0) } }
set { shadowColor = newValue?.cgColor }
}
}
-
CALayerのエクステンションを作成し、IBファイル専用プロパティを追加。
CALayerのshadowColor
やborderColor
はUIColorではなくCGColorのプロパティであるため、ラップしてUIColorアクセスできるようにする。加えて、Swiftコードからアクセスできないようアノテーションも付加する。 -
UserDefinedRuntimeAttributesで、上記プロパティを用いて色指定。
対応例3: Paddingを調整できるUILabelを作りたい
@IBDesignable final class PaddingLabel: UILabel {
// MARK: Property
@IBInspectable var horizontalPadding: CGFloat = 0
@IBInspectable var verticalPadding: CGFloat = 0
// MARK: Lifecycle
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
invalidateIntrinsicContentSize()
}
override init(frame: CGRect) {
super.init(frame: frame)
invalidateIntrinsicContentSize()
}
override var intrinsicContentSize: CGSize {
var contentSize = super.intrinsicContentSize
contentSize.width += horizontalPadding * 2
contentSize.height += verticalPadding * 2
return contentSize
}
}
- IBDesignable対応のUILabel継承クラスを作成。
- Paddingを設定できるようなIBInspectableプロパティを追加。
-
intrinsicContentSize
(推奨サイズ)について、上記プロパティ値を反映するようオーバーライド - IBファイルで
intrinsicContentSize
が反映されるよう、イニシャライザにinvalidateIntrinsicContentSize()
を付加
対応例4: UITextViewのPadding等を調整できるようにして、UILabel同等に使いたい
extension UITextView {
@available(*, unavailable, message: "Only use it at Storybord or Xib.", renamed: "textContainerInset.top")
@IBInspectable var topPadding: Float {
get { return Float(self.textContainerInset.top) }
set { textContainerInset.top = CGFloat(newValue) }
}
@available(*, unavailable, message: "Only use it at Storybord or Xib.", renamed: "textContainerInset.left")
@IBInspectable var leftPadding: Float {
get { return Float(self.textContainerInset.left + 5) }
set { textContainerInset.left = CGFloat(newValue - 5) }
}
@available(*, unavailable, message: "Only use it at Storybord or Xib.", renamed: "textContainerInset.right")
@IBInspectable var rightPadding: Float {
get { return Float(self.textContainerInset.right + 5) }
set { textContainerInset.right = CGFloat(newValue - 5) }
}
@available(*, unavailable, message: "Only use it at Storybord or Xib.", renamed: "textContainerInset.bottom")
@IBInspectable var bottomPadding: Float {
get { return -1 * Float(self.textContainerInset.bottom) }
set { textContainerInset.bottom = -1 * CGFloat(newValue) }
}
@available(*, unavailable, message: "Only use it at Storybord or Xib.", renamed: "textContainer.maximumNumberOfLines")
@IBInspectable var lines: Int {
get { return textContainer.maximumNumberOfLines }
set { textContainer.maximumNumberOfLines = newValue }
}
}
- UITextViewのextensionを作成。
- Paddingと行数最大値を設定できるようなIBInspectableプロパティを追加。
- IBファイルで下記設定をする。
- Paddingの各値を0に。
-
Lines
を指定。 -
Scrolling Enabled
をfalseに指定。
アンチパターン🙅
🙅1: CALayerのプロパティをUIView系のファイルのIBInspectableに付加
@IBDesignable final class AntiPatternDesignableView1: UIView {
@IBInspectable var cornerRadius: Float {
get { return Float(layer.cornerRadius) }
set { layer.cornerRadius = CGFloat(newValue) }
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override init(frame: CGRect) {
super.init(frame: frame)
}
}
UserDefinedRuntimeAttributesで「layer.~」で指定すれば済む。以下、「付加すべき」とした場合のデメリット。
-
UIViewのextensionに付加すべき
付加した設定項目が全Viewのインスペクタに出てきてしまいカオス。実装の邪魔になる。 -
基底クラスを作ってそこに付加すべき:
1と同じデメリットに加え、基底クラスを作ることでデバッグ時の影響範囲特定を困難にする。 -
各UIView系クラスで付加すべき
- クラスごとに都度実装しなければならなくなる。
- IBファイルでこのクラスを使った場合、SwiftファイルのIBOutletはそのクラスにしなければならなくなるため、デバッグ時や改修時にそのクラスがどれぐらい特殊かを確認・記憶する必要が出てくる。
- なお、「実装者が付加したかったら…」というチーム方針にすると、UserDefinedRuntimeAttributesとIBInspectableの2パターンの実装が混在するのでデバッグの阻害要因になる。
**結論:**CALayerのプロパティは、UserDefinedRuntimeAttributesでアクセスすべきで、UIViewにIBInspectableで別インターフェースを作るべきではない。
🙅2: イニシャライザ実装の代わりにawakeFromNib実装
@IBDesignable final class AntiPatternDesignableView2: UIView {
override func awakeFromNib() {
super.awakeFromNib()
}
}
IBファイル(StoryboardやXib)経由で生成した場合には期待通り生成されるが、コードで生成した場合には期待通り生成されない。
**結論:**イニシャライザ実装 is 正義
Tips🌞
🌞1: レンダリングを手動実行にする方法
レンダリング処理に引きづられてマシンが重い場合は、Editor > AutomaticallyRefreshViews
のチェックを外して、レンダリングを手動実行にする。(レンダリング実行するときはその下のRefreshAllViews
を選択)
🌞2: レンダリングされない場合のチェックリスト
No. | 項目 | 確認箇所 | 内容 | 備考 |
---|---|---|---|---|
1 | レンダリング中ではないか確認 | IBファイル(StoryboardやXib)のビュー利用箇所:Inspector(=ウインドウの右ビューエリア) > IdentityInspector(上部アイコンの左から3つ目) > CustomViews > Designables | Updatingではないことを確認。 | |
2 | 再レンダリングして再確認 | - | メニューバー>Editor>RefreshAllViewsを選んで再レンダリング。 | |
3 | キャッシュクリア、Xcode再起動して再確認 | - | キャッシュクリア(⌘+shift+k)やXcode再起動をして再確認。 | |
4 | Swiftファイル実装の確認 | Swiftファイル | アノテーションの付加はされているか。継承元クラスのRequiredInitをすべて実装しているか。 | |
5 | レイアウト制約エラーの確認 | Navigator(=ウインドウの左ビューエリア)>ReportNavigator(=上部アイコンの右端)>InterfaceBuilder | レイアウト制約(Constraint)が不適当だと、レンダリング失敗する。 | |
6 | その他実装のエラーの確認 | Navigator(=ウインドウの左ビューエリア)>ReportNavigator(=上部アイコンの右端)>InterfaceBuilder | プロジェクト全体のビルドの後にレンダリングするので、無関係の箇所のエラーも失敗原因になる。 | 以前、失敗原因が"通常ビルドではスルーされる通信ライブラリのビルドエラー"だったときもあった。 |
7 | 他の開発環境でも起こるか確認 | - | 他PCで再確認。 |
備考
汎用で使える分はGist: ShoichiKuraoka/IBDesignableExtension.swiftにまとめてあるので、よければ。