Edited at

XIBからViewを生成する4つの実装パターン


はじめに

XIBファイルからカスタムViewを生成する方法はいくつかあります。

XIBファイルのFile's OwnerやViewのclassをどのように指定し、そこから生成したViewをどのようにclassに紐づけていくのかを以下の4つのパターンで記載していこうと思います。

リンク
Files's Owner
View Class
ViewのIBOutlet紐付け
利用法

1-1
指定なし (NSObject)
ProfileView
指定なし
UINib.inistantiate(nil).first as! ProfileView

1-2
指定なし (NSObject)
_ProfileView
指定なし
UINib.inistantiate(nil).first as! _ProfileViewをProfileViewにaddSubView

2-1
ProfileView
指定なし (UIView)
指定なし
UINib.inistantiate(self).first as! UIViewをProfileViewにaddSubView

2-2
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は指定されていない状態になっています。これらの状態をベースの話をすすめていこうと思います。

スクリーンショット 2018-09-07 13.48.42.png


1-1 UINib.inistantiate(nil).firstを直接つかう


XIB

ViewのclassをProfileViewに指定します。

スクリーンショット 2018-08-29 13.52.43.png

UILabelはViewであるProfileViewに対してIBOutletで紐付けます。

スクリーンショット 2018-08-29 13.52.59.png


実装

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とほぼ同じですが、XIBのViewと紐付ける_ProfileViewとProfileViewを分けています。


XIB

Viewのclassを_ProfileViewに指定します。

スクリーンショット 2018-08-31 13.49.08.png

UILabelはViewである_ProfileViewに対してIBOutletで紐付けます。

スクリーンショット 2018-08-31 13.49.30.png


実装

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をクラスに紐付けるでは、XIBファイルのViewにclassを紐付けていましたが、XIBのFile's Ownerにclassを紐付けます。


2-1 UINib.inistantiate(self).firstをaddSubview


XIB

File's OwnerのclassをProfileViewに指定します。

スクリーンショット 2018-08-31 13.56.09.png

UILabelはFile's OwnerであるProfileViewに対してIBOutletで紐付けます。

スクリーンショット 2018-08-31 13.56.34.png

Viewのclassを未指定のままにします。

スクリーンショット 2018-08-31 13.56.56.png


実装

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に指定します。

スクリーンショット 2018-08-31 14.01.41.png

UILabelはFile's OwnerであるProfileViewのusernameLabelに、Viewは_viewに対してIBOutletで紐付けます。

スクリーンショット 2018-08-31 14.01.56.png

Viewのclassを未指定のままにします。

スクリーンショット 2018-08-31 14.02.13.png


実装

XibのViewはFile's Owner(ProfileView)の_viewに紐付いているため、UINib(nibName: "ProfileView", bundle: nil).instantiate(withOwner: self, options: nil)をした際にロードされます。

よって、2-1のように、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をクラスに紐付ける場合は、自身がXIBのViewになっているあるいは明確にレイアウトとしてclassが分かれているのでコード上からも追いやすいなと思っています。

XibのFile's Ownerをクラスに紐付ける場合は、XIBのviewは別viewになっていますが、紐付いた明示的な別クラスがあるわけではなくただのUIViewなので、構造を理解するに要する時間がXibのviewをクラスに紐付ける場合よりもかかってしまうと思っています。

ただ慣れれば、2-2 UINib.inistantiate(self)してviewをIBOutletで紐付けるが使いやすいと思います。