はじめに
本記事はMobileAct TOKYO #5で発表した以下の内容をまとめたものになります。
※スライド版はこちらです。
- Storyboard、XIB、コードベースといったiOSにおけるレイアウト手法の比較
- コードベースレイアウトの6つのTips
- Githubのサンプルコード
比較表
比較項目 | Storyboard | XIB | コードベース |
---|---|---|---|
➡️ Segueが使える | ✅ | ||
👀 GUIで見てわかる | ✅ | ✅ | |
📱 画面サイズの分岐が簡単 | ✅ | ✅ | |
🔎 静的チェック | ✅ | ✅ | |
💉コンストラクタDI | ✅ | ✅ | |
♻️ 再利用性 | ✅ | ||
💥 コンフリクトしにくい | ✅ | ||
🛂 レビューしやすさ | ✅ |
StoryboardとXIBの特徴
良いところ
- GUIで操作が直感的
- Storyboardの場合はSegueが使える
- 複数の画面サイズでの見え方がわかる
- Size Classesによる画面サイズによる分岐がしやすい
- 静的な制約チェック
イマイチなところ
- 部分的にコピペしたり、カスタムクラス化するなど再利用・変更がしづらい
- コンフリクトの解消が難しい
- XMLなのでコードレビューが難しい
コードベースレイアウトの特徴
良いところ
- コードの再利用がしやすい
- コピペやコード置換などでスピーディに修正が可能
- コンフリクトの解消がしやすい
- レビューがしやすい
イマイチなところ
- UIKitのオブジェクトのプロパティの知識やライフサイクルに関する知識が必要
- 実行するまで見え方は想像でしかわからない
コンストラクタインジェクションの違い
Storyboardでは初期化処理がUIStoryboardクラスによって隠蔽されるため、コンストラクタインジェクションを行うことができません。
let storyboard = UIStoryboard(name: "ViewController", bundle: nil)
let storyboardViewController = storyboard.instantiateInitialViewController()
逆にXIB、コードベースレイアウトの場合は以下のように記述することでコンストラクタインジェクションが可能です。
class ViewController: UIViewController {
let dependency: Dependency
init(dependency: Dependency) {
self.dependency = dependency
super.init(nibName: nil, bundle: nil)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
もちろんStoryboardでもプロパティインジェクションは可能ですが、プロパティをvarやOptional型にする必要が出てくるため、可能ならば避けたいです。
コードベースレイアウトの6つのTips
- Initialization Closure
- Then
- カスタムクラス
- lazy var
- SnapKit / PureLayout
- UIStackView
1. Initialization Closure
コードベースレイアウトを安直に行なった場合、プロパティの設定コードやレイアウトコードが多くなり、コードが汚くなりがちです。
class ViewController: UIViewController {
let priceLabel = UILabel()
let imageView = UIImageView()
override func viewDidLoad() {
super.viewDidLoad()
priceLabel.numberOfLines = 2
priceLabel.textColor = .red
priceLabel.font = .boldSystemFont(ofSize: 14)
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
imageView.layer.cornerRadius = 4
// addSubview, AutoLayout...
}
}
Initialization Closure(初期化クロージャ)によってプロパティの初期化処理をまとめることができます。
class ViewController: UIViewController {
let priceLabel: UILabel = {
let label = UILabel()
label.numberOfLines = 2
label.textColor = .red
label.font = .boldSystemFont(ofSize: 14)
return label
}()
let imageView: UIImageView = {
let view = UIImageView()
view.contentMode = .scaleAspectFill
view.clipsToBounds = true
view.layer.cornerRadius = 4
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
// addSubview, AutoLayout...
}
}
2. Then
viewDidLoadのコードは減りましたが、まだ少し冗長なコードになっています。
class ViewController: UIViewController {
let priceLabel: UILabel = { // 型アノテーション
let label = UILabel() // ローカルスコープでの命名
label.numberOfLines = 2 // label
label.textColor = .red // label
label.font = .boldSystemFont(ofSize: 14) // label
return label // 生成したインスタンスのreturn
}()
let imageView: UIImageView = { // 型アノテーション
let view = UIImageView() // ローカルスコープでの命名
view.contentMode = .scaleAspectFill // view
view.clipsToBounds = true // view
view.layer.cornerRadius = 4 // view
return view // 生成したインスタンスのreturn
}()
override func viewDidLoad() {
super.viewDidLoad()
// addSubview, AutoLayout...
}
}
オープンソースライブラリのdevxoul/Thenを利用することで、よりシンプルに書くことができます。
class ViewController: UIViewController {
let priceLabel = UILabel().then {
$0.numberOfLines = 2
$0.textColor = .red
$0.font = .boldSystemFont(ofSize: 14)
}
let imageView = UIImageView().then {
$0.contentMode = .scaleAspectFill
$0.clipsToBounds = true
$0.layer.cornerRadius = 4
}
override func viewDidLoad() {
super.viewDidLoad()
// addSubview, AutoLayout...
}
}
Then自体は1ファイルのとてもミニマムなライブラリでthen関数の実装は以下のようになっています。
public protocol Then {}
extension Then where Self: AnyObject {
public func then(_ block: (Self) throws -> Void) rethrows -> Self {
try block(self)
return self
}
}
3. カスタムクラス
使い回すViewであればUIViewを継承したカスタムクラスを定義することで再利用性を高め、よりシンプルに記述できます。
class ViewController: UIViewController {
let priceLabel = PriceLabel()
let imageView = ItemImageView()
override func viewDidLoad() {
super.viewDidLoad()
}
}
多くのUI部品を表示する画面でもカスタムクラス化によってクラスを小さくし、分割統治をすることでより保守性を高められるかと思います。
4. lazy var
プロパティの初期化時にselfや他のプロパティを参照したい場合があります。
private let captureButton = CaptureButton().then {
$0.onTapped = { [weak self] in
self?.camera.capture()
}
}
プロパティの初期化時にはselfはまだ初期化されていないため、コンパイルエラーとなります。
private let captureButton = CaptureButton().then {
$0.onTapped = { [weak self] in // コンパイルエラー
self?.camera.capture() // コンパイルエラー
}
}
そのような場合、lazy var
で定義することで初期化処理をプロパティの最初の参照時に遅延できるため、selfを利用することができます。
private lazy var captureButton = CaptureButton().then {
$0.onTapped = { [weak self] in
self?.camera.capture() // OK
}
}
5. SnapKit / PureLayout
次はコードによってAutoLayoutを記述します。
iOS9から利用可能なNSLayoutAnchorで記述すると以下のようになります。
view.addSubview(button)
button.leadingAnchor.constraintEqualToAnchor(view.leadingAnchor).active = true
button.trailingAnchor.constraintEqualToAnchor(view.trailingAnchor).active = true
button.topAnchor.constraintEqualToAnchor(view.topAnchor).active = true
button.bottomAnchor.constraintEqualToAnchor(view.bottomAnchor).active = true
記述量が多くてとても大変です🤮
オープンソースライブラリのSnapKitやPureLayoutを利用することで、コードはかなりシンプルになります。
view.addSubview(button)
button.snp.makeConstraints { $0.edges.equalToSuperview() }
その他の記述例はこのような感じです。
// 読みやすい
button.snp.makeConstraints {
$0.center.equalToSuperview() // 親ビューの中心に
$0.size.equalTo(CGSize(width: 64, height: 64)) // サイズ指定
}
// Safe Areaにもレイアウトを貼りやすい
tableView.snp.makeConstraints {
$0.top.bottom.equalTo(view.safeAreaLayoutGuide) // 上下はSafe Areaに
$0.leading.trailing.equalToSuperview() // 左右は親ビューに
}
6. UIStackView
UIStackViewクラスはViewを縦横に積むことができるレイアウト要素を持ったViewのため、UIStackViewを活用することで制約がシンプルになります。
stackView.addArrangedSubview(label)
stackView.addArrangedSubview(imageView)
stackView.addArrangedSubview(button)
label.snp.makeConstraints { $0.height.equalTo(20) }
button.snp.makeConstraints { $0.height.equalTo(60) }
UIStackViewをネストさせることもできますが、階層構造が深くなるとわかりにくくなるため、個人的な目安としては、1クラスにつき2階層程度にし、階層が深くなった場合は別クラスに切り出すと良いかと思います。
まとめ
- コードベースレイアウトでもTipsを使えば簡潔かつスピーディに書ける
- どの手法もできるようにしておき、使い分けたい
例)
- シンプルなレイアウト要件にはコードベース
- 複雑なレイアウト要件にはXIB
- Segueを利用したい場合にはStoryBoard
サンプルコード
Storyboard、 XIB、 コードベースレイアウトのサンプルコードです。
https://github.com/shtnkgm/iOSUILayoutMethods
ViewController(画面)ごとに以下の異なる手法でレイアウトを実装しました。
- Storyboard
- XIB
- コードベース(Thenを利用した初期化)
- NSLayoutAnchorを利用したAutoLayout
- PureLayoutを利用したAutoLayout
- SnapKitを利用したAutoLayout