iOSとコードベースレイアウト

# はじめに
本記事は[MobileAct TOKYO #5](https://mobileact.connpass.com/event/114398/)で発表した以下の内容をまとめたものになります。
※スライド版は[こちら](https://speakerdeck.com/shtnkgm/iosfalsekodobesureiauto)です。

 - Storyboard、XIB、コードベースといったiOSにおけるレイアウト手法の比較
 - コードベースレイアウトの6つのTips
 - Githubのサンプルコード

# 比較表

|比較項目|Storyboard|XIB|コードベース|
|:--|:--:|:--:|:--:|
|➡️ Segueが使える|✅|||
|👀 GUIで見てわかる|✅|✅||
|📱 画面サイズの分岐が簡単|✅|✅||
|🔎 静的チェック|✅|✅||
|💉コンストラクタDI||✅|✅|
|♻️ 再利用性|||✅|
|💥 コンフリクトしにくい|||✅|
|🛂 レビューしやすさ|||✅|

# StoryboardとXIBの特徴

**良いところ**

 - GUIで操作が直感的
 - Storyboardの場合はSegueが使える
 - 複数の画面サイズでの見え方がわかる
 - Size Classesによる画面サイズによる分岐がしやすい
 - 静的な制約チェック

**イマイチなところ**

 - 部分的にコピペしたり、カスタムクラス化するなど再利用・変更がしづらい
 - コンフリクトの解消が難しい
 - XMLなのでコードレビューが難しい

# コードベースレイアウトの特徴

**良いところ**

 - コードの再利用がしやすい
 - コピペやコード置換などでスピーディに修正が可能
 - コンフリクトの解消がしやすい
 - レビューがしやすい

**イマイチなところ**

 - UIKitのオブジェクトのプロパティの知識やライフサイクルに関する知識が必要
 - 実行するまで見え方は想像でしかわからない

# コンストラクタインジェクションの違い

Storyboardでは初期化処理がUIStoryboardクラスによって隠蔽されるため、コンストラクタインジェクションを行うことができません。

```swift
let storyboard = UIStoryboard(name: "ViewController", bundle: nil)
let storyboardViewController = storyboard.instantiateInitialViewController()
```

逆にXIB、コードベースレイアウトの場合は以下のように記述することでコンストラクタインジェクションが可能です。

```swift
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

 1. Initialization Closure
 2. Then
 3. カスタムクラス
 4. lazy var
 5. SnapKit / PureLayout
 6. UIStackView

# 1. Initialization Closure

コードベースレイアウトを安直に行なった場合、プロパティの設定コードやレイアウトコードが多くなり、コードが汚くなりがちです。

```swift
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（初期化クロージャ）によってプロパティの初期化処理をまとめることができます。

```swift
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のコードは減りましたが、まだ少し冗長なコードになっています。

```swift
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](https://github.com/devxoul/Then)を利用することで、よりシンプルに書くことができます。

```swift
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関数の実装は以下のようになっています。

```swift
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を継承したカスタムクラスを定義することで再利用性を高め、よりシンプルに記述できます。

```swift
class ViewController: UIViewController {
    let priceLabel = PriceLabel()
    let imageView = ItemImageView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
}
```

多くのUI部品を表示する画面でもカスタムクラス化によってクラスを小さくし、分割統治をすることでより保守性を高められるかと思います。

# 4. lazy var

プロパティの初期化時にselfや他のプロパティを参照したい場合があります。

```swift
private let captureButton = CaptureButton().then {
    $0.onTapped = { [weak self] in
        self?.camera.capture()
    }
}
```

プロパティの初期化時にはselfはまだ初期化されていないため、コンパイルエラーとなります。

```swift
private let captureButton = CaptureButton().then {
    $0.onTapped = { [weak self] in // コンパイルエラー
        self?.camera.capture()     // コンパイルエラー
    }
}
```

そのような場合、`lazy var`で定義することで初期化処理をプロパティの最初の参照時に遅延できるため、selfを利用することができます。

```swift
private lazy var captureButton = CaptureButton().then {
    $0.onTapped = { [weak self] in
        self?.camera.capture()    // OK
    }
}
```

# 5. SnapKit / PureLayout

次はコードによってAutoLayoutを記述します。

iOS9から利用可能なNSLayoutAnchorで記述すると以下のようになります。

```swift
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](https://github.com/SnapKit/SnapKit)や[PureLayout](https://github.com/PureLayout/PureLayout)を利用することで、コードはかなりシンプルになります。

```swift
view.addSubview(button)
button.snp.makeConstraints { $0.edges.equalToSuperview() }
```

その他の記述例はこのような感じです。

```swift
//  読みやすい
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](https://developer.apple.com/documentation/uikit/uistackview)クラスはViewを縦横に積むことができるレイアウト要素を持ったViewのため、UIStackViewを活用することで制約がシンプルになります。

```swift
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


<img width="200px" src="https://qiita-image-store.s3.amazonaws.com/0/113553/2c2f08af-0740-3365-5ae2-b60faa271552.png">