@IBDesignable を指定した View の中で Asset Catalog の内容を読み取るために必要なこと

# 概要

`@IBDesignable` を指定したカスタム View クラスの中で `UIColor` クラスのイニシャライザ `init(named:)` を呼び出す処理を書いておき、Interface Builder 内で、ある View Controller クラスの View にそのカスタム View クラスを 追加したところ、以下のようなビルドエラーが発生しました。

```
IB Designables: Failed to render and update auto layout status for [View Controller のクラス名] ([Interface Builder 内での View Controller の Object ID]): The agent crashed
```

この問題の原因について調査したところ、 `@IBDesignable` を指定したカスタム View クラスの `prepareForInterfaceBuilder()` から呼び出される処理の中で Asset Catalog の内容を正しく読み出されないためであることがわかりました。

`prepareForInterfaceBuilder()` から呼び出される処理の中でも Asset Catalog の内容を正しく読み取れるように設定することにより、 Asset Catalog 内で定義した色情報を使用できるようになりました。

# エラーログの格納場所

本記事が対象とするエラーの詳細ログは `~/Library/Logs/DiagnosticReports` ディレクトリ内の `IBDesignablesAgent-iOS_YYYY-MM-DD-HHMMDD_[マシン名].crash` に記録されています。

# `@IBDesignable` を指定したカスタム View の定義方法

`@IBDesignable` を指定したカスタム View クラスを使用することにより、 Interface Builder 上でカスタム View のプレビューをするなどの利点があります。

`@IBDesinable` を使用する利点や使用方法については、下記リンク先の記事が参考になります。

[カスタムコンポーネントを使用したUI実装について - ZOZO Technologies TECH BLOG](https://techblog.zozo.com/entry/ios_custom_component)

# `prepareForInterfaceBuilder()` から Asset Catalog の内容を読み取る

`prepareForInterfaceBuilder()` から呼び出される処理の中で `UIColor` クラスのイニシャライザ `init(named:)` を呼び出すとビルドエラーとなるのは、 `init(named:)` では Asset Catalog の内容を正しく読み取ることができないためです。

Asset Catalog の内容を正しく読み取るためには `init(named:)` の代わりに `init(named:in:compatibleWith:)` を使って、 対象の Bundle を明示的に指定する必要があります。具体的なコードの例を下に示します。

```swift:ビルドエラーとなる例
@IBDesignable final class RoundedRectButton: UIButton {
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupAttributes()
    }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setupAttributes()
    }
    
    override func prepareForInterfaceBuilder() {
        super.prepareForInterfaceBuilder()
        setupAttributes()
    }
    
    private func setupAttributes() {
        clipsToBounds = true
        layer.cornerRadius = 8.0
        layer.borderWidth = 2.0
        layer.borderColor = UIColor.black.cgColor
        // この実装だとビルドエラー
        backgroundColor = UIColor(named: "MyColor")!
    }
}
```

```swift:ビルド成功となる例
@IBDesignable final class RoundedRectButton: UIButton {
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupAttributes()
    }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setupAttributes()
    }
    
    override func prepareForInterfaceBuilder() {
        super.prepareForInterfaceBuilder()
        setupAttributes()
    }
    
    private func setupAttributes() {
        clipsToBounds = true
        layer.cornerRadius = 8.0
        layer.borderWidth = 2.0
        layer.borderColor = UIColor.black.cgColor
        // この実装だとビルド成功
        backgroundColor = UIColor(named: "MyColor", in: Bundle(for: type(of: self)), compatibleWith: nil)!
    }
}
```

`prepareForInterfaceBuilder()` から呼び出される処理の中では、 Asset Catalog 内の画像を使用する場合にも同様に、 `UIImage` クラスのイニシャライザには `init(named:)` ではなく `init(named:in:with:)` を使う必要があります。

# その他の対応方法

今回着目したビルドエラーは上記対応方法にて解決できるのですが、もしその他の理由により `prepareForInterfaceBuilder()` 内で実行する処理とそれ以外を区別したい場合には、 `TARGET_INTERFACE_BUILDER` というプリプロセッサマクロが役に立ちます。

```swift:TARGET_INTERFACE_BUILDERの使用例
#if TARGET_INTERFACE_BUILDER
    // prepareForInterfaceBuilder()内でのみ有効となる
#else
    // prepareForInterfaceBuilder()内以外でのみ有効となる
#endif
```

# 参考情報

* [ios - How to load an image in prepareForInterfaceBuilder with a IBDesignable UIImageView - Stack Overflow](https://stackoverflow.com/questions/24671770/how-to-load-an-image-in-prepareforinterfacebuilder-with-a-ibdesignable-uiimagevi)
* [IBInspectable / IBDesignable - NSHipster](https://nshipster.com/ibinspectable-ibdesignable/)