🍎

カスタム View / ViewController の生成方法まとめ

8 min read

はじめに

iOSのUIKitを利用した開発では、開発者が実装したカスタムView/ViewControllerが使われています。
これらは生成する側、生成される側でそれぞれ適切な処理を記述しないと、 @IBOutlet / @IBAction の紐付けに失敗し、クラッシュしたり正しく画面に描画されないといった問題があります。

  • 新規でアプリを作るとき
  • 他人が実装した既存のView/ViewControllerを利用するとき

など必要になったタイミングで毎回調べていたのですが、開発を効率よく進めるために生成方法についてまとめました。

GitHubにサンプルのプロジェクトを用意しています。

https://github.com/kitwtnb/GenerateCustomViewAndViewControllerSample

コードでレイアウトされたViewの生成

@IBOutlet@IBAction が使われていない、コードでレイアウトされたViewは普通にイニシャライザで生成します。

View

CodeOnlyView.swift
class CodeOnlyView: UIView {
    let label = UILabel()
}

生成時に引数を渡す場合、 required init?(coder: NSCoder) も一緒に実装しないとビルドエラーになります。

CodeOnlyWithArgumentView.swift
class CodeOnlyWithArgumentView: UIView {
    let label = UILabel()

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    init(arg: Any?) {
        super.init(frame: .zero)
    }
}

ViewController

ViewController.swift
let view = CodeOnlyView()

xibの File's Owner にクラスを指定したViewを @IBOutlet で生成

init?(coder:) でxibファイルを読み込み、自身に addSubview() します。

View

XibFilesOwnerView.swift
class XibFilesOwnerView: UIView {
    @IBOutlet weak var label: UILabel!

    required init?(coder: NSCoder) {
        super.init(coder: coder)

        let nibName = "xibファイル名(拡張子なし)"

        let view = UINib(nibName: nibName, bundle: nil).instantiate(withOwner: self, options: nil).first as? UIView
        // Bundleから読み込む場合はこちら
        // let view = Bundle.main.loadNibNamed(nibName, owner: self, options: nil)?.first as? UIView

        if let view = view {
            view.frame = self.bounds
            self.addSubview(view)
        }
    }
}

ViewController

ViewController.swift
@IBOutlet weak var view: XibFilesOwnerView!

xibの File's Owner にクラスを指定したViewをコードで生成

イニシャライザでxibファイルを読み込み、自身に addSubview() します。
こちらはイニシャライザをオーバーライドする必要がないため、生成時にパラメータを渡すことが可能です。

View

XibFilesOwnerView.swift
class XibFilesOwnerView: UIView {
    @IBOutlet weak var label: UILabel!

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    init(arg: Any?) {
        super.init(frame: .zero)

        let nibName = "xibファイル名(拡張子なし)"

        let view = UINib(nibName: nibName, bundle: nil).instantiate(withOwner: self, options: nil).first as? UIView
        // Bundleから読み込む場合はこちら
        // let view = Bundle.main.loadNibNamed(nibName, owner: self, options: nil)?.first as? UIView

        if let view = view {
            view.frame = self.bounds
            self.addSubview(view)
        }
    }
}

ViewController

ViewController.swift
let view = XibFilesOwnerView(arg: ...)

xibでルートViewの Custom Class にクラスを指定したViewを @IBOutlet で生成

awakeAfter(using:) の中でxibファイルを読み込みます。

View

XibCustomClassView.swift
class XibCustomClassView: UIView {
    @IBOutlet weak var label: UILabel!
    
    override func awakeAfter(using coder: NSCoder) -> Any? {
        guard subviews.isEmpty else { return self }

        let nibName = "xibファイル名(拡張子なし)"

        let view = UINib(nibName: nibName, bundle: nil).instantiate(withOwner: nil, options: nil).first
        // Bundleから読み込む場合はこちら
        // let view = Bundle.main.loadNibNamed(nibName, owner: nil, options: nil)?.first

        return view
    }
}

ViewController

ViewController.swift
@IBOutlet weak var view: XibCustomClassView!

xibでルートViewの Custom Class にクラスを指定したViewをコードで生成

UINib#instantiate(withOwner: nil, options: nil).first または Bundle#loadNibNamed(nibName, owner: nil, options: nil)?.first でxibファイルから読み込み、生成します。
ViewControllerが直接生成することも可能ですが、Viewがstatic funcで生成すると初期化処理を隠蔽できます。

View

XibCustomClassView.swift
class XibCustomClassView: UIView {
    @IBOutlet weak var label: UILabel!
    
    static func load(arg: Any?) -> Self {
        let nibName = "xibファイル名(拡張子なし)"

        let view = UINib(nibName: nibName, bundle: nil).instantiate(withOwner: nil, options: nil).first as! Self
        // Bundleから読み込む場合はこちら
        // let view = Bundle.main.loadNibNamed(nibName, owner: nil, options: nil)?.first as! Self

        view.configure(arg: arg)
        return view
    }

    private func configure(arg: Any?) {
        ...
    }
}

ViewController

ViewController.swift
let xibCustomClassViewFromCode = XibCustomClassView.load(arg: ...)

コードでレイアウトされたViewControllerの生成

@IBOutlet@IBAction が使われていない、コードでレイアウトされたViewControllerは普通にイニシャライザで生成します。

遷移先のViewController

CodeOnlyViewController.swift
class CodeOnlyViewController: UIViewController {
    let label = UILabel()
}

生成時に引数を渡す場合、 required init?(coder: NSCoder) も一緒に実装しないとビルドエラーになります。

CodeOnlyWithArgumentViewController.swift
class CodeOnlyWithArgumentViewController: UIViewController {
    let label = UILabel()

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    init(arg: Any?) {
        super.init(nibName: nil, bundle: nil)
    }
}

遷移元のViewController

ViewController.swift
let viewController = CodeOnlyViewController()

xibでレイアウトされたViewControllerをコードで生成

init(nibName:, bundle:) を呼び出して生成します。
イニシャライザを追加することでDIが可能です。

遷移先のViewController

XibViewController.swift
class XibViewController: UIViewController {
    @IBOutlet weak var label: UILabel!

    required init?(coder: NSCoder) { super.init(coder: coder) }
    init(arg: Any?) {
        let nibName = "xibファイル名(拡張子なし)"
        super.init(nibName: nibName, bundle: nil)
    }
}

遷移元のViewController

ViewController.swift
let viewController = XibViewController(arg: ...)

StoryboardでレイアウトされたViewControllerをコードで生成

UIStoryboard#instantiateViewController(identifier:, creator:) を呼び出して生成します。
第2引数の creator: ((NSCoder) -> ViewController?)? で遷移先のViewControllerのイニシャライザを呼び出せるため、ここでDIが可能です。

instantiateViewController(identifier:, creator:) はiOS 13から利用可能
https://developer.apple.com/documentation/uikit/uistoryboard/3213989-instantiateviewcontroller

遷移先のViewController

StoryboardViewController.swift
class StoryboardViewController: UIViewController {
    @IBOutlet weak var label: UILabel!

    required init?(coder: NSCoder) { fatalError() }
    init?(coder: NSCoder, arg: Any?) {
        super.init(coder: coder)
    }
}

遷移元のViewController

ViewController.swift
// 生成するViewControllerが同じStoryboard上に存在する場合は
// 自身の `storyboard` プロパティを利用可能
let storyboardName = "Storyboardファイル名(拡張子なし)"
let storyboard = UIStoryboard(name: storyboardName, bundle: nil)

let identifier = "Storyboard上でViewControllerに設定したIdentifier"

let viewController = storyboard.instantiateViewController(identifier: identifier) { coder in
    StoryboardViewController(coder: coder, arg: ...)
}

参考記事

https://qiita.com/marty-suzuki/items/7c7ecdcb1f16c21b0051
https://medium.com/@shiba1014/xibとコードの紐付け方まとめ-200da7766306

変更履歴

  • 2021/8/29
    • CodeOnlyViewControllerUIViewController を継承していなかったのを修正
    • コードでレイアウトされたView, ViewControllerの生成時に引数を渡す場合に required init?(coder: NSCoder) が必要であることを記載

Discussion

ログインするとコメントできます