Edited at

Swift IBDesignable入門

プロジェクト変わる度に同じ議論をしていて、もうこれはまとめた方が良いなと。。

ば.png


概要

IBDesignable(およびIBInspectable)への対応方法まとめ。


基本


IBDesignable、IBInspectableとは

IBファイル(StoryboardやXib)の実装がとてもしやすくなる機能。

(なお、これ以外のメリットはない🌛)



  • 登場以前:カスタムビューをIBファイル(StoryboardやXib)上で使う際「実機上ではきちんと表示されるものの、IBファイル上では真っ白なビュー」という状態だったため、新規開発はおろか追加開発もデバッグもしづらかった。


  • 登場後:カスタムビューでもIBファイル上で実機のようにレンダリングしてくれるようになり、さらにIBInspectableによってカスタムインターフェースを追加できるようになった。


IBDesignableを実装する方法


IBDesignable例:レンダリングだけするUIViewカスタムクラス

@IBDesignable final class DesignableView: UIView {

required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override init(frame: CGRect) {
super.init(frame: frame)
}
}

やることは2つ。

1. クラス宣言の頭に@IBDesinnableアノテーションを付加する。

2. イニシャライザを実装する。


IBInspectableを実装する方法


IBInspectable例:UITextViewのTopPaddingを調整可能に

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


実装で気をつけるべきこと



  1. 標準インターフェースと被るIBInspectableは作らない


    • 標準インターフェースと被るIBInspectableを実装すると、後々のデバッグの邪魔になる。

    • 仕方なく作る場合は、IBファイルでのみ使うよう警告アノテーションを付加する。




  2. 基底クラスは作らない


    • 基底クラスを作ると後々のデバッグで影響範囲の特定が困難になる。

    • なお、チームの方針によるが、個人的には、IBDesignable/IBInspectableの実装を簡単にするためだけに基底クラスを作るのは微妙かと。




  3. IBファイルでのみ使うクラスやプロパティには警告アノテーションを付加


    • IBDesignable/IBInspectableの目的は「IBファイルの実装をアシストする」ことでしかないので、これらに関する実装でSwiftファイル側のコードが散らかるのはデバッグ観点で避けるべき。

    • よって、実装する際、IBファイルでIBファイル専用Viewクラス(例: CustomLabel)を使う場合は、SwiftファイルのIBOutletは標準クラス(例: UILabel)にしたりする方が望ましい。

    • ただし、チーム内で「実装気をつけてね」というルールを作ってもだいたい忘れたりするため、警告アノテーションを付加してSwiftファイル側でそれらを使った場合にビルド失敗するようにする。

    • @available(*, unavailable, message: "やっちゃったね。これはIBファイル専用クラスだよ。Swiftファイルで参照する際は継承元クラスで参照してね👴", renamed: "UILabel")




対応例


対応例1: 影や楕円のついたビューを作りたい


レンダリングだけするUIViewカスタムクラス(アノテーション付き)

// 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)
}
}

Clip.png

Att.png



  1. 利用Viewを継承したIBファイル(StoryboardやXib)専用クラスを作成


    • 描画専門となるようアノテーション付加とイニシャライザ実装以外はしない。

    • ちなみに、拡張することはないクラスなので、見通しよくなるようUIViewやUILabelなどの各拡張クラスは1ファイルにまとめていいかと。




  2. Clip to Bounds(「Viewの中身を切り取るか」の指定)を指定


    • 影をつける場合はfalse、角丸にする場合はtrueにする。なお、コードで指定する場合はlayer.masksToBoundsで指定。

    • 影付きかつ角丸のUILabelやUIImageViewを作りたい場合は、Clip to Bounds = falseの角丸&影付きのUIViewを作成し、その上にClip to Bounds = trueで角丸指定した対象ビューを置く。



  3. 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: 影や枠線に色をつけたい


影や枠線に色をつけられるようなCALayerExtension(アノテーション付き)

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 }
}
}


スクリーンショット 2018-12-29 14.50.03.png


  1. CALayerのエクステンションを作成し、IBファイル専用プロパティを追加。

    CALayerのshadowColorborderColorはUIColorではなくCGColorのプロパティであるため、ラップしてUIColorアクセスできるようにする。加えて、Swiftコードからアクセスできないようアノテーションも付加する。


  2. UserDefinedRuntimeAttributesで、上記プロパティを用いて色指定。



対応例3: Paddingを調整できるUILabelを作りたい


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
}
}



  1. IBDesignable対応のUILabel継承クラスを作成。

  2. Paddingを設定できるようなIBInspectableプロパティを追加。


  3. intrinsicContentSize(推奨サイズ)について、上記プロパティ値を反映するようオーバーライド

  4. IBファイルでintrinsicContentSizeが反映されるよう、イニシャライザにinvalidateIntrinsicContentSize()を付加


対応例4: UITextViewのPadding等を調整できるようにして、UILabel同等に使いたい


UITextViewのPadding等を調整可能に

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 }
}
}

スクリーンショット 2018-12-29 15.23.02.png


  1. UITextViewのextensionを作成。

  2. Paddingと行数最大値を設定できるようなIBInspectableプロパティを追加。

  3. 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.~」で指定すれば済む。以下、「付加すべき」とした場合のデメリット。


  1. UIViewのextensionに付加すべき

    付加した設定項目が全Viewのインスペクタに出てきてしまいカオス。実装の邪魔になる。


  2. 基底クラスを作ってそこに付加すべき

    1と同じデメリットに加え、基底クラスを作ることでデバッグ時の影響範囲特定を困難にする。



  3. 各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: レンダリングを手動実行にする方法

dd.png

レンダリング処理に引きづられてマシンが重い場合は、Editor > AutomaticallyRefreshViewsのチェックを外して、レンダリングを手動実行にする。(レンダリング実行するときはその下のRefreshAllViewsを選択)


🌞2: レンダリングされない場合のチェックリスト

スクリーンショット 2018-12-29 15.52.16.png

ssss.png

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にまとめてあるので、よければ。