soranoba
soranoba Author of soranoba.net
programming

Xib攻略ガイド

XcodeにはInterface Builderと呼ばれるUI構築ツールがあります.
Xibには色々な設定方法があり, 私が使う上で守っている自己ルールを紹介します.


なぜXibを使うのか?

現在, Xibの他にStoryboardやSwiftUIという選択肢がありますが, 私はxibしか使いません. その理由はxibが一番ストレスなく, 必要十分な機能が提供されているからです.

Storyboard

現在は解消されている可能性もありますが, UITableViewのStaticCellsで@IBDesignableなViewを組み込むと正しいサイズではないというエラーが頻繁に発生するという問題がありました.
また, Storyboardを使用するとViewの初期化がSwiftのInitializerを使うことができなくなり, 初期化方法の明記が困難になるという問題もあります.

SwiftUI

Xibを使うシーンでは, Viewのサイズが変わった際に「constraintsが壊れないか」「正しい見た目になるか」を確認したいことが多く, SwiftUIではこれを行うことが困難に感じました.
また, iOSのバージョンに制限が入るという点も大きなデメリットです.

Xib

Storyboardと比べると, いくつかできないことがあるものの必要十分な機能は提供されています.


Xibとクラスの紐付け

実際にXibで構築したUIを使用する為には, クラスとの紐付けが必要です.
私は全て統一的に扱う為に, File's Ownerにクラスを紐付けています. これは, UIViewControllerの場合に他に選択肢がない為で, UITableViewCellのような場合でも同様に設定するようにしています.
このような初期化を行うとXibからinstantiateされたViewが隠蔽され, UIViewが一つ余分に生成されたりしますが, そういったことは気にしません.
クラス名の設定

UIViewController

UIViewControllerの場合はUIViewController.viewIBOutletに紐付けを行い, 以下のように初期化を行いします.

public init() {
    super.init(nibName: "SomeViewController", bundle: Bundle(for: type(of: self)))
}

@available(*, unavailable)
public required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
}

UIView

UIViewの場合はSizeをFreeformに変更し, 以下のように初期化を行います. Freeformに設定する

// MARK: - Lifecycle

override public init(frame: CGRect) {
    super.init(frame: frame)
    setup()
}

public required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    setup()
}

private func setup() {
    let nib = UINib(nibName: "SomView", bundle: Bundle(for: type(of: self)))
    guard let view = nib.instantiate(withOwner: self, options: nil).first as? UIView else {
        fatalError("Failed to load nib")
    }
    
    view.frame = bounds
    view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
    addSubview(view)
}

UITableViewCellやUICollectionViewCellの場合は, contentViewのsubviewにする必要がある為, 設定方法が変わります.
layoutMarginsGuideを使用するべき場合は, contentView.layoutMarginsGuideの各アンカーを利用します.

// MARK: - Lifecycle

override public init(frame: CGRect) {
    super.init(frame: frame)
    setup()
}

public required init?(coder: NSCoder) {
    super.init(coder: coder)
    setup()
}

private func setup() {
    let nib = UINib(nibName: "SomViewCell", bundle: Bundle(for: type(of: self)))
    guard let view = nib.instantiate(withOwner: self, options: nil).first as? UIView else {
        fatalError("Failed to load nib")
    }
    
    view.translatesAutoresizingMaskIntoConstraints = false
    contentView.addSubview(view)
    NSLayoutConstraint.activate([
        contentView.topAnchor.constraint(equalTo: view.topAnchor),
        contentView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
        contentView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
        contentView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
    ])
}

これをUITableViewに登録する場合は第1引数がXibではなく, クラスのregister(_:forCellReuseIdentifier:)を使う点に注意してください. つまり以下のように書きます.

tableView.register(SomeViewCell.self, forCellReuseIdentifier: reuseIdentifier)

これで全ての場合にInitializerで初期化できるようになり, コードから生成する際も直感的に利用できるようになります.

Viewのサイズを決定する方法

iOS開発において, Viewのサイズを決定する方法としていくつかの手段がありますが, 大きく分けると2つに分類されます.

  • Viewの中のコンテンツのサイズに応じてサイズ決定をする
  • NSLayoutConstraintなどの制約によってサイズを決定する

後者だけを使用する場合は簡単ですが, 前者を利用するシーンにおいて, この区別ができていないと制約の設定が正しく行えなくなる為, 明確に区別しておく必要があります.

Viewの中のコンテンツサイズに応じてサイズ決定をする

代表的な例としてUILabelが上げられます. Constraintによる制約をつけない限りUILabelはテキストに合わせてサイズに合わせて自動的にサイズが変更されます. これは, intrinsicContentSizeによってサイズが決定されています.

この値はContent Hugging PriorityContent Compression Resistance Priorityによって制約のpriorityが決定されます.
つまり, これらの値より高いpriorityを持つconstraintよりも優先度が低くなる点に注意が必要です.

また, @IBDesignableの独自クラスでintrinsicContentSizeがXib上で確定できない場合は, Intrinsic Sizeから値を設定することで初期値を設定することができます.

Content Priority

子のコンテンツサイズを伝搬する

子のコンテンツサイズを利用しようとする場合, 上記のContent Prioirtyを高く設定することで利用することができます.
しかし, 子のUILabelにMarginを2pxずつ追加したコンテンツサイズにしたいとした場合に, 2pxのconstraintを貼ってしまうと複数のconstraintによって構成される複雑なpriorityになってしまい, Viewを増やしていった時に整合性を取るのが難しくなります.

そこで, 子のコンテンツサイズを伝搬するUIStackView@IBDesignableな独自クラスを作成・使用することでシンプルなpriorityで表現できるようになります.

例えばPaddingを追加するUILabelは以下のように実装することができます.

// MARK: - UIView (Overrides)

override public var intrinsicContentSize: CGSize {
    let size = super.intrinsicContentSize
    return CGSize(width: size.width + paddingLeft + paddingRight,
                  height: size.height + paddingTop + paddingBottom)
}

// MARK: - UILabel (Overrides)

override public func drawText(in rect: CGRect) {
    let paddingInsets = UIEdgeInsets(top: paddingTop, left: paddingLeft, bottom: paddingBottom, right: paddingRight)
    super.drawText(in: rect.inset(by: paddingInsets))
}

UIStackViewの子の優先度を指定する

UIStackViewのサイズが変更された場合に, 子のサイズ決定に使用されるのは前述のContent Hugging PriorityContent Compression Resistance Priorityです.
1ずつpriorityをずらすことで, どの順番に優先するかを指定します.

constraintのpriorityをずらす

constraintのpriorityを常に1000にしてしまうと, その制約が満たせない場合にconstraintが外れてしまいます.
Viewのサイズを拡大・縮小するとエラーが出る場合はこのリスクがあるので, 左右もしくは上下のいずれかのpriorityを999などにすることで, これを避けることができます.

サイズを拡大・縮小する

IBDesignableの利用

IBDesignableのクラスを作成する

Xib上で独自クラスを使用するには@IBDesignableを使用します. これは単にクラスにアノテーションを付けるだけです.

@IBDesignable public class SomeView: UIView {
}

例えばユーザーのリストを表示する@IBDesignableなクラスを作成し, Xib上で何人かのユーザーが表示されている状態の表示をしたい場合は以下のような実装を加えることで実現できます.

// MARK: - UIView (Overrides)

override public func prepareForInterfaceBuilder() {
    super.prepareForInterfaceBuilder()

    users = [
        .init(userId: "Alice", userName: "Alice", avatarImageURL: nil),
        .init(userId: "Bob", userName: "Bob", avatarImageURL: nil),
        .init(userId: "Carol", userName: "Carol", avatarImageURL: nil),
    ]
}

IBDesignableのクラスを使用する

そのクラスの継承元をXibに追加し (UILabelを継承していたらUILabel, UIViewの場合はUIView), 「Xibとクラスの紐付け」と同様にクラスを設定するだけで利用することができます.
@IBInspectableのアノテーションを付与したプロパティはXib上から値で設定できるので, 積極的に活用しましょう.

IBDesignableのクラッシュ

XCode12以前は~/Library/Logs/DiagnosticReportsIBDesignableのクラッシュログが出力されていました. しかし, XCode12からは出力されなくなりデバッグが困難になりました.
運良く以下のようにDebugボタンが表示されている場合は確認することができます. そうでない場合は, XCodeを立ち上げ直したると出るようになることがあるようです.

IBDesignableのクラッシュ
(再現がうまくできなかった為, こちらから画像を引用しています)


Enjoy good Xcode life!

(Updated: )

comments powered by Disqus