xyk blog

最近は iOS 開発の記事が多めです。

IBDesignable を使ったカスタムビューのプレビュー時に呼ばれるメソッドについて

検証環境:
Xcode 11.3.1
Swift 5.1.3

IBDesignable や IBInspectable を使うことで Storyboard や Xib 上でカスタムの属性の設定ができ、変更がリアルタイムに反映されてプレビューできるようになります。
ところで、IBOutlet で接続したビューに対して初期設定を行う場合には接続完了後のタイミングで呼ばれる UIView.awakeFromNib() 内でやるのがセオリーかと思いますが、 Storyboard や Xib 上のプレビュー時にはこのメソッドが呼ばれません。
その代わりに UIView.prepareForInterfaceBuilder() が呼ばれるのでこちらにも同様の初期設定のコードを書いておくと実行時と同じような処理結果にすることができます。

プレビュー時とランタイム(実行)時で呼ばれるメソッドの違いについて以下の検証用コードを作って試してみました。

検証用コード

import UIKit

@IBDesignable
open class MyCustomView: UIView {
    
    var items: [String] = []
    
    @IBInspectable
    open var prop1: String? {
        didSet {
            items.append("IBInspectable prop1")
        }
    }
    
    @IBInspectable
    open var prop2: Int = 0 {
        didSet {
            items.append("IBInspectable prop2")
        }
    }
    
    override public init(frame: CGRect) {
        super.init(frame: frame)
        items.append("init(frame:)")
    }
    
    required public init?(coder: NSCoder) {
        super.init(coder: coder)
        items.append("init?(coder:)")
    }
    
    override public func awakeFromNib() {
        super.awakeFromNib()
        items.append("awakeFromNib")
    }
    
    override public func prepareForInterfaceBuilder() {
        super.prepareForInterfaceBuilder()
        items.append("prepareForInterfaceBuilder")
    }
    
    override open func updateConstraints() {
        super.updateConstraints()
        items.append("updateConstraints")
    }

    override public var intrinsicContentSize: CGSize {
        items.append("intrinsicContentSize")
        return super.intrinsicContentSize
    }
    
    override open func layoutSubviews() {
        super.layoutSubviews()
        items.append("layoutSubviews")
        configure()
    }
    
    private func configure() {
        guard subviews.count == 0 else { return }
        let textView = UITextView()
        textView.isEditable = false
        addSubview(textView)
        textView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            textView.topAnchor.constraint(equalTo: topAnchor),
            textView.bottomAnchor.constraint(equalTo: bottomAnchor),
            textView.leadingAnchor.constraint(equalTo: leadingAnchor),
            textView.trailingAnchor.constraint(equalTo: trailingAnchor),
        ])
        let result = items.enumerated().map { "\($0 + 1). \($1)" }.joined(separator: "\n")
        print(result)
        textView.text = result
    }
}

Storyboard プレビュー時

1. init(frame:)
2. IBInspectable prop1
3. IBInspectable prop2
4. prepareForInterfaceBuilder
5. updateConstraints
6. layoutSubviews

まずinit?(coder:)ではなく、init(frame:)が呼ばれます。
そしてawakeFromNibは呼ばれず、代わりにprepareForInterfaceBuilderが呼ばれます。
AutoLayout を設定してますが、intrinsicContentSizeは呼ばれないようです。

ランタイム(実行)時

こちらは上記StoryBoardで設定したものが実際に実行された時の結果になります。

1. init?(coder:)
2. IBInspectable prop1
3. IBInspectable prop2
4. awakeFromNib
5. intrinsicContentSize
6. updateConstraints
7. layoutSubviews

コードからカスタムビュー生成(Frame指定)

以下はおまけで同じカスタムビューをコードで生成した場合も試してみました。

let myCustomView = MyCustomView(frame: .init(origin: .zero, size: .init(width: 150, height: 150)))
myCustomView.prop1 = "Hello"
myCustomView.prop2 = 100
view.addSubview(myCustomView)

結果

1. init(frame:)
2. IBInspectable prop1
3. IBInspectable prop2
4. updateConstraints
5. layoutSubviews

コードからカスタムビュー生成(AutoLayout指定)

let myCustomView = MyCustomView()
myCustomView.prop1 = "Hello"
myCustomView.prop2 = 100
view.addSubview(myCustomView)
myCustomView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
    myCustomView.topAnchor.constraint(equalTo: view.topAnchor),
    myCustomView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
    myCustomView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
    myCustomView.heightAnchor.constraint(equalToConstant: 150),
])

結果

1. init(frame:)
2. IBInspectable prop1
3. IBInspectable prop2
4. intrinsicContentSize
5. updateConstraints
6. layoutSubviews

関連

UICollectionViewCell や UICollectionReusableView に IBDesignable なカスタムビューを置いた場合、プレビュー時に layoutSubviews が呼ばれないケースがあるので注意。

xyk.hatenablog.com