カスタム View / ViewController の生成方法まとめ
はじめに
iOSのUIKitを利用した開発では、開発者が実装したカスタムView/ViewControllerが使われています。
これらは生成する側、生成される側でそれぞれ適切な処理を記述しないと、 @IBOutlet
/ @IBAction
の紐付けに失敗し、クラッシュしたり正しく画面に描画されないといった問題があります。
- 新規でアプリを作るとき
- 他人が実装した既存のView/ViewControllerを利用するとき
など必要になったタイミングで毎回調べていたのですが、開発を効率よく進めるために生成方法についてまとめました。
GitHubにサンプルのプロジェクトを用意しています。
コードでレイアウトされたViewの生成
@IBOutlet
や @IBAction
が使われていない、コードでレイアウトされたViewは普通にイニシャライザで生成します。
View
class CodeOnlyView: UIView {
let label = UILabel()
}
生成時に引数を渡す場合、 required init?(coder: NSCoder)
も一緒に実装しないとビルドエラーになります。
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
let view = CodeOnlyView()
File's Owner
にクラスを指定したViewを @IBOutlet
で生成
xibの init?(coder:)
でxibファイルを読み込み、自身に addSubview()
します。
View
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
@IBOutlet weak var view: XibFilesOwnerView!
File's Owner
にクラスを指定したViewをコードで生成
xibの イニシャライザでxibファイルを読み込み、自身に addSubview()
します。
こちらはイニシャライザをオーバーライドする必要がないため、生成時にパラメータを渡すことが可能です。
View
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
let view = XibFilesOwnerView(arg: ...)
Custom Class
にクラスを指定したViewを @IBOutlet
で生成
xibでルートViewの awakeAfter(using:)
の中でxibファイルを読み込みます。
View
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
@IBOutlet weak var view: XibCustomClassView!
Custom Class
にクラスを指定したViewをコードで生成
xibでルートViewの UINib#instantiate(withOwner: nil, options: nil).first
または Bundle#loadNibNamed(nibName, owner: nil, options: nil)?.first
でxibファイルから読み込み、生成します。
ViewControllerが直接生成することも可能ですが、Viewがstatic funcで生成すると初期化処理を隠蔽できます。
View
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
let xibCustomClassViewFromCode = XibCustomClassView.load(arg: ...)
コードでレイアウトされたViewControllerの生成
@IBOutlet
や @IBAction
が使われていない、コードでレイアウトされたViewControllerは普通にイニシャライザで生成します。
遷移先のViewController
class CodeOnlyViewController: UIViewController {
let label = UILabel()
}
生成時に引数を渡す場合、 required init?(coder: NSCoder)
も一緒に実装しないとビルドエラーになります。
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
let viewController = CodeOnlyViewController()
xibでレイアウトされたViewControllerをコードで生成
init(nibName:, bundle:)
を呼び出して生成します。
イニシャライザを追加することでDIが可能です。
遷移先のViewController
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
let viewController = XibViewController(arg: ...)
StoryboardでレイアウトされたViewControllerをコードで生成
UIStoryboard#instantiateViewController(identifier:, creator:)
を呼び出して生成します。
第2引数の creator: ((NSCoder) -> ViewController?)?
で遷移先のViewControllerのイニシャライザを呼び出せるため、ここでDIが可能です。
遷移先のViewController
class StoryboardViewController: UIViewController {
@IBOutlet weak var label: UILabel!
required init?(coder: NSCoder) { fatalError() }
init?(coder: NSCoder, arg: Any?) {
super.init(coder: coder)
}
}
遷移元のViewController
// 生成する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: ...)
}
参考記事
変更履歴
- 2021/8/29
-
CodeOnlyViewController
がUIViewController
を継承していなかったのを修正 - コードでレイアウトされたView, ViewControllerの生成時に引数を渡す場合に
required init?(coder: NSCoder)
が必要であることを記載
-
Discussion