iOS
xib
Swift

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で紐付けるが使いやすいと思います。