@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

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

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

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

ビルドエラーとなる例
@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")!
    }
}
ビルド成功となる例
@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 というプリプロセッサマクロが役に立ちます。

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

参考情報