LoginSignup
187
147

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-09-07

はじめに

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

187
147
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
187
147