【Swift】Initialization ClosureでviewDidLoadの肥大化を防ぐ

メインの内容は完全に[こちらの記事](https://thatthinginswift.com/kill-your-viewdidload/)の受け売りですが、勉強になったので、整理を兼ねて残します。
間違えた解釈や補足などあればご教授ください :raised_hands: 

※2016/06/29：`lazy var`の使用について追記しました。
※2017/10/03：Swift4の書き方に修正しました。

# Initialization Closure とは？

本題に入る前に、Initialization Closureについて調べてみました。

ストアドプロパティ(Stored property)の初期値を与える際に、**初期値となる値を返すクロージャ**の**実行結果**を与えることができ、このような書き方を**Initialization Closure**というようです。

恐らくコードを見た方が早いと思うので、公式リファレンスのサンプルコードを載せます。

```swift
struct Chessboard {
    let boardColors: [Bool] = {
        var temporaryBoard = [Bool]()
        var isBlack = false
        for i in 1...8 {
            for j in 1...8 {
                temporaryBoard.append(isBlack)
                isBlack = !isBlack
            }
            isBlack = !isBlack
        }
        return temporaryBoard
    }()
    func squareIsBlackAt(row: Int, column: Int) -> Bool {
        return boardColors[(row * 8) + column]
    }
}
```

*リファレンスは[こちら](https://developer.apple.com/library/mac/documentation/Swift/Conceptual/Swift_Programming_Language/Initialization.html#//apple_ref/doc/uid/TP40014097-CH18-ID232)*

この例では、チェスのボードの各行・列におけるカラー情報（trueが黒, falseが白）を保持したArrayを Initialization Closure で初期値として与えています。
つまり、`let boardColors`の初期値はクロージャ内で算出された`temporaryBoard`の値になります。

`boardColors`は前述の Initialization Closure で初期化されているので、下記のように使用できます。

```swift
let board = Chessboard()
print(board.squareIsBlackAt(row: 0, column: 1))
// Prints "true"
print(board.squareIsBlackAt(row: 7, column: 7))
// Prints "false"
```

### 注意点

- クロージャの最後に`()`をつけること
    - `()`をつけることで、クロージャが即実行され、ストアドプロパティにクロージャから返った値が代入されます。
- クロージャの中では`self`や他のプロパティ、メソッドを使用できません
    - クロージャの実行時にはまだそれぞれ初期化されていないためです。


# `viewDidLoad`の肥大化を防ぐ

ここからが本題ですが、はじめに述べたように、完全に[こちらの記事](https://thatthinginswift.com/kill-your-viewdidload/)の受け売りです。笑

まずは、従来の書き方を見てみましょう。（サンプルコードも[参考サイト](https://thatthinginswift.com/kill-your-viewdidload/)を元にSwift4の書き方に修正）

## 従来の書き方

```swift
class ViewController: UIViewController {
    let topView = UIView()
    let imageView = UIImageView()
    let goButton = UIButton()

    override func viewDidLoad() {
        topView.frame = CGRect(x: 0, y: 0, width: 100, height: 200)
        topView.backgroundColor = UIColor.red
        view.addSubview(topView)

        imageView.image = UIImage(named: "profile")
        topView.addSubview(imageView)

        goButton.frame = CGRect(x: 0, y: 0, width: 30, height: 30)
        goButton.setTitle("GO!", for: .normal)
        view.addSubview(goButton)
    }
}
```

`topView`や`imageView`など、`ViewController`内の他の場所でも使用したい場合には、「まずviewの初期化だけしておき、`viewDidLoad`で細かい設定をした上で`addSubview`する」という書き方をします。

私もよくやることがあります。

### 問題点

この書き方での問題点としては、`viewDidLoad`で「各viewの設定」「各viewの設置」という２種類のことを行うことから、viewの数が増える分だけコードの見通しの悪さを生じさせる点です。


そこで、 Initialization Closure を活用し、「各viewの設定」を切り分けることで`viewDidLoad`を簡潔に書くことができるようです。

## Initialization Closure の活用

※上記同様に[参考サイト](https://thatthinginswift.com/kill-your-viewdidload/)を元にSwift4の書き方に修正

```swift
class ViewController: UIViewController {
    let topView: UIView = {
        let view = UIView()
        view.frame = CGRect(x: 0, y: 0, width: 100, height: 200)
        view.backgroundColor = UIColor.red
        return view
    }()

    let imageView: UIImageView = {
        let imageView = UIImageView()
        imageView.image = UIImage(named: "profile")
        return imageView
    }()

    let goButton: UIButton = {
        let button = UIButton()
        button.frame = CGRect(x: 0, y: 0, width: 30, height: 30)
        button.setTitle("GO!", for: .normal)
        return button
    }()

    override func viewDidLoad() {
        view.addSubview(topView)
        topView.addSubview(imageView)
        view.addSubview(goButton)
    }
}
```

こちらでは、「各viewの設定」がそれぞれのクロージャ内で行われ、それぞれのプロパティは**設定済みのview**を初期値で持つことができるので、`viewDidLoad`内ではいきなり`addSubview`することができます。

これによって、`viewDidLoad`では「各viewの設置」を行うという役割に専念できています。
どこで何がされているかがとても分かりやすくなったと思います。

# 終わりに

これを書いている段階では、簡単なサンプルは動かしてみましたが、実際のアプリケーションの中で使ってはいないので、これから取り入れていこうと思います。

実際に取り入れてみないとわからないこともあるかと思いますが、ひとまず「こういう書き方ができるんだ〜」という発見と、「Initialization Closure」についての整理という意味で書き残しました。

いろんな書き方ができて面白いですね！ :blush: 

# 参考
https://thatthinginswift.com/kill-your-viewdidload/

# 追記（2016/06/29）

## `lazy var`を使用した`Initialization Closure`でselfを使う

上記の例における、`let`を使用して`Initialization Closure`で初期化するプロパティは、そのクラスや構造体の初期化が完了する前に初期値が確定している必要があります。
そのことからも、通常のプロパティの初期化同様、`Initialization Closure`による初期化もそのクラスや構造体の初期化完了前に実行されることは明白です。
そのため上記例だと、初期化の完了していない`self`を`Initialization Closure`内で使用することはできません。

しかし、コメントにていただいたように`lazy var`を使用することでこの問題は解決できます。

`lazy var`を使用して宣言したプロパティは、 **`lazy stored property`**といい、クラスや構造体の初期化時には初期化されず、初めてそのプロパティにアクセスした時に初期値がセットされます。

つまり、`lazy stored property`の初期化タイミングはそのクラスや構造体が初期化された後になることがわかります。
そのため、`lazy stored property`の初期値を`Initialization Closure`でセットする際には、そのクロージャ内で`self`を使用することができるようになります。

---

例のごとく、[参考サイト](https://thatthinginswift.com/kill-your-viewdidload/
)のコードだとこのような感じです。

```swift
class ViewController: UIViewController {
    lazy var imagePickerController: ImagePickerController = {
        let picker = ImagePickerController()
        picker.delegate = self // ←selfが初期化されているので使える
        picker.imageLimit = 1
        return picker
    }()
}
```

下記はエラーになる。

```swift
class ViewController: UIViewController {
    let imagePickerController: ImagePickerController = {
        let picker = ImagePickerController()
        picker.delegate = self // ←selfが初期化前なので使えない
        picker.imageLimit = 1
        return picker
    }()
}
```


## 追記参照
- `lazy`については[こちら](http://blog.sclap.info/entry/swift-how-to-use-lazy)が分かりやすかったです
- `lazy`に関する[公式ドキュメント](https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Properties.html#//apple_ref/doc/uid/TP40014097-CH14-ID255)