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.view
のIBOutlet
に紐付けを行い, 以下のように初期化を行いします.
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に変更し, 以下のように初期化を行います.
// 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 Priority
とContent Compression Resistance Priority
によって制約のpriorityが決定されます.
つまり, これらの値より高いpriorityを持つconstraintよりも優先度が低くなる点に注意が必要です.
また, @IBDesignable
の独自クラスでintrinsicContentSize
がXib上で確定できない場合は, Intrinsic Size
から値を設定することで初期値を設定することができます.
子のコンテンツサイズを伝搬する
子のコンテンツサイズを利用しようとする場合, 上記の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 Priority
とContent 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/DiagnosticReports
にIBDesignable
のクラッシュログが出力されていました. しかし, XCode12からは出力されなくなりデバッグが困難になりました.
運良く以下のようにDebugボタンが表示されている場合は確認することができます. そうでない場合は, XCodeを立ち上げ直したると出るようになることがあるようです.
(再現がうまくできなかった為, こちらから画像を引用しています)
Enjoy good Xcode life!
記事が気に入ったらチップを送ることができます!
You can give me a cup of coffee :)
Kyash ID: soranoba
Amazon: Wish List
GitHub Sponsor: github.com/sponsors/soranoba
PayPal.Me: paypal.me/soranoba
(Updated: )