はじめに
XIBファイルからカスタムViewを生成する方法はいくつかあります。
XIBファイルのFile's OwnerやViewのclassをどのように指定し、そこから生成したViewをどのようにclassに紐づけていくのかを以下の4つのパターンで記載していこうと思います。
リンク | Files's Owner | View Class | ViewのIBOutlet紐付け | 利用法 |
---|---|---|---|---|
[1-1](1-1 UINib.inistantiate(nil).firstを直接つかう) | 指定なし (NSObject) | ProfileView | 指定なし | UINib.inistantiate(nil).first as! ProfileView |
[1-2](1-2 UINibとViewを別クラスに) | 指定なし (NSObject) | _ProfileView | 指定なし | UINib.inistantiate(nil).first as! _ProfileViewをProfileViewにaddSubView |
[2-1](2-1 UINib.inistantiate(self).firstをaddSubview) | ProfileView | 指定なし (UIView) | 指定なし | UINib.inistantiate(self).first as! UIViewをProfileViewにaddSubView |
[2-2](2-2 UINib.inistantiate(self)してviewをIBOutletで紐付ける) | ProfileView | 指定なし (UIView) | ProfileViewの_view | UINib.inistantiate(self)し、IBOutletで紐付けた_viewをProfileViewにaddSubView |
ここではView上にUILabelが載っているProfileViewというclassを実装します。
※ 1.Bundle.loadNibNamedではなく、UINib(nibName:bundle:)を使う前提にしています。
※ 2.view同じサイズでaddSubviewするコードを書く際に、長くなってしまうのでNSLayoutConstraintではなくAutoresizingMaskを利用してます。
1. Xibのviewをクラスに紐付ける
下図のようにNew File
する際に、UITableViewCellなどのsubclassとXIBも生成する設定で追加を行うと、XIBファイル上のViewのclassは自動的にそのsubclassに指定されます。このとき、File's Ownerは指定されていない状態になっています。これらの状態をベースの話をすすめていこうと思います。
1-1 UINib.inistantiate(nil).firstを直接つかう
XIB
ViewのclassをProfileViewに指定します。
UILabelはViewであるProfileViewに対してIBOutletで紐付けます。
実装
XIBファイル上のViewがProfileViewと紐付いていてViewは一つしか存在しないため、UINib(nibName: "ProfileView", bundle: nil).instantiate(withOwner: nil, options: nil).first
を呼び出すとProfileViewが返ってきます。
この場合、ProfileViewは既にインスンタンス化されているのでinitializerは利用できないため、クラス外から何かしらの値を受け取ってpropertyで保持する場合には、Optional(またはImplicitly Unwrapped Optional)でpropertyを定義する必要があります。
そのため、Viewをロードした後にpropertyに任意の値を代入をしてからProfileView返すstatic関数を定義する形になるかと思います。
また、ProfileViewはXIBからロードしてインスタンス化しているため、init?(coder aDecoder: NSCoder)
とawakeFromNib
が呼ばれます。
final class ProfileView: UIView {
@IBOutlet weak var usernameLabel: UILabel!
private(set) var user: User!
static func make(user: User) -> ProfileView {
let view = UINib(nibName: "ProfileView", bundle: nil)
.instantiate(withOwner: nil, options: nil)
.first as! ProfileView
view.user = user
return view
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func awakeFromNib() {
super.awakeFromNib()
}
}
1-2 UINibとViewを別クラスに
[1-1](1-1 UINib.inistantiate(nil).firstを直接つかう)とほぼ同じですが、XIBのViewと紐付ける_ProfileViewとProfileViewを分けています。
XIB
Viewのclassを_ProfileViewに指定します。
UILabelはViewである_ProfileViewに対してIBOutletで紐付けます。
実装
ProfileViewが実際に利用するclass、_ProfileViewはレイアウトのclassです。
_ProfileViewはUINib(nibName: "ProfileView", bundle: nil).instantiate(withOwner: nil, options: nil).first
から取得し、ProfileViewで保持します。
ProfileViewでは、_ProfileViewで持っているものと同じpropertyをcomputed propertyで返すようにしています。
また_ProfileViewはProfileViewと同じサイズでaddSubviewしています。
利用するclassとレイアウトのclassを分けることで、利用する側のclassのinitializerが使えるようになります。
この場合、ロード時には_ProfileViewのinit?(coder aDecoder: NSCoder)
とawakeFromNib
が呼ばれます。
final class ProfileView: UIView {
var usernameLabel: UILabel { return _view.usernameLabel }
private let _view: _ProfileView
let user: User
init(user: User) {
self._view = UINib(nibName: "ProfileView", bundle: nil)
.instantiate(withOwner: nil, options: nil)
.first as! _ProfileView
self.user = user
super.init(frame: .zero)
_view.frame = bounds
addSubview(_view)
_view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
}
}
final class _ProfileView: UIView {
@IBOutlet fileprivate weak var usernameLabel: UILabel!
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func awakeFromNib() {
super.awakeFromNib()
}
}
2. XibのFile's Ownerをクラスに紐付ける
[Xibのviewをクラスに紐付ける](## 1. Xibのviewをクラスに紐付ける)では、XIBファイルのViewにclassを紐付けていましたが、XIBのFile's Ownerにclassを紐付けます。
2-1 UINib.inistantiate(self).firstをaddSubview
XIB
File's OwnerのclassをProfileViewに指定します。
UILabelはFile's OwnerであるProfileViewに対してIBOutletで紐付けます。
Viewのclassを未指定のままにします。
実装
XIBでFile's OwnerをProfileViewに指定しているので、super.init
後にUINib.instantiate(withOwner: self, options: nil)として紐付けを行っています。
UILabelはFile's Owner(ProfileView)に対して紐付いているため、この時点でロードが行われます。
Viewのclassはコンテンツが載っているただのUIViewなので、UINib.instantiate(self).first
から取得しProfileViewに対してaddSubviewします。
ProfileViewは自身のinitialize後に、XIBのFile's Ownerとして紐付けを行うので、ProfileViewのinitializerが使えます。
Viewとしてクラスに紐付いていないため、init?(coder aDecoder: NSCoder)
とawakeFromNib
は呼ばれません。
final class ProfileView: UIView {
@IBOutlet private(set) weak var usernameLabel: UILabel!
let user: User
init(user: User) {
self.user = user
super.init(frame: .zero)
let _view = UINib(nibName: "ProfileView", bundle: nil)
.instantiate(withOwner: self, options: nil)
.first as! UIView
_view.frame = bounds
addSubview(_view)
_view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
}
}
2-2 UINib.inistantiate(self)してviewをIBOutletで紐付ける
XIB
File's OwnerのclassをProfileViewに指定します。
UILabelはFile's OwnerであるProfileViewのusernameLabelに、Viewは_viewに対してIBOutletで紐付けます。
Viewのclassを未指定のままにします。
実装
XibのViewはFile's Owner(ProfileView)の_viewに紐付いているため、UINib(nibName: "ProfileView", bundle: nil).instantiate(withOwner: self, options: nil)
をした際にロードされます。
よって、[2-1](2-1 UINib.inistantiate(self).firstをaddSubview)のように、UINib.instantiate(self)
.firstでviewを取得する必要はなくなります。
このviewの紐付き方は、New File
からUIViewControllerとXIBを同時に生成した場合に自動生成されるXIBの設定と同等のものになります。
また、この場合もViewはProfileViewではなくてコンテンツが載っているただのUIViewなので、didSetでProfileViewにaddSubviewしています。
Viewとしてクラスに紐付いていないため、init?(coder aDecoder: NSCoder)
とawakeFromNib
は呼ばれません。
final class ProfileView: UIView {
@IBOutlet private(set) var _view: UIView! {
didSet {
_view.frame = bounds
addSubview(_view)
_view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
}
}
@IBOutlet private(set) weak var usernameLabel: UILabel!
let user: User
init(user: User) {
self.user = user
super.init(frame: .zero)
UINib(nibName: "ProfileView", bundle: nil)
.instantiate(withOwner: self, options: nil)
}
}
また以下の実装のようにXIBからロードする用のView classを作成することで、XIBを利用する各classごとに_viewを定義する必要がなくなります。
final class ProfileView: XibView {
@IBOutlet weak var usernameLabel: UILabel!
let user: User
init(user: User) {
self.user = user
super.init(frame: .zero)
}
}
class XibView: UIView {
class var nibName: String {
return String(describing: self)
}
@IBOutlet private var _view: UIView! {
didSet {
_view.frame = bounds
addSubview(_view)
_view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
}
}
override init(frame: CGRect) {
super.init(frame: frame)
UINib(nibName: type(of: self).nibName, bundle: nil)
.instantiate(withOwner: self, options: nil)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
UINib(nibName: type(of: self).nibName, bundle: nil)
.instantiate(withOwner: self, options: nil)
}
}
個人的な感想
[Xibのviewをクラスに紐付ける](## 1. Xibのviewをクラスに紐付ける)場合は、自身がXIBのViewになっているあるいは明確にレイアウトとしてclassが分かれているのでコード上からも追いやすいなと思っています。
[XibのFile's Ownerをクラスに紐付ける](## 2. XibのFile's Ownerをクラスに紐付ける)場合は、XIBのviewは別viewになっていますが、紐付いた明示的な別クラスがあるわけではなくただのUIViewなので、構造を理解するに要する時間が[Xibのviewをクラスに紐付ける](## 1. Xibのviewをクラスに紐付ける)場合よりもかかってしまうと思っています。
ただ慣れれば、[2-2 UINib.inistantiate(self)してviewをIBOutletで紐付ける](### 2-2 UINib.inistantiate(self)してviewをIBOutletで紐付ける)が使いやすいと思います。